feat(shim): add task wrapper and containerd access session#657
Conversation
✅ Deploy Preview for urunc canceled.
|
|
I opened this draft PR for the initial task service wrapper and narrow containerd access session. There are still some implementation details to clean up, but I would like to first check whether the overall design direction looks reasonable to you. Please let me know if this layering matches what we discussed, or if you think I should adjust the structure before continuing with the remaining details. Thanks! |
f9fcc7b to
530c6dd
Compare
cmainas
left a comment
There was a problem hiding this comment.
Hello @sidneychang ,
I have added a few comments in the code. Please correct me if I have misunderstood something.
Also, is it necessary to wrap the manager too?
| namespace string | ||
| containerID string | ||
|
|
||
| mu sync.Mutex |
There was a problem hiding this comment.
Why do we need a mutex? As far as I understand we will have one container, which is going to get initialized once and then it should immutable.
|
|
||
| // Container returns the task-level container metadata, fetching it once from | ||
| // containerd and reusing it for subsequent feature helpers. | ||
| func (s *ContainerdSession) Container(ctx context.Context) (*containersapi.Container, error) { |
There was a problem hiding this comment.
Since we are working in the task level we are handling one container per time. Therefore, we can add the lofic of obtaining the metadata when we create anew session.
Hello, @cmainas My idea was to keep all the shim wrapper logic in this PR, so the later view feature can build on top of it. If we prefer not to introduce it before it is actually used, I can drop it here and add the manager wrapper back in the view feature PR. |
530c6dd to
a888c83
Compare
jiwahn
left a comment
There was a problem hiding this comment.
Hi Sidney, thanks for this implementation. I think this aligns really well with the direction we discussed in the issue. I left comment regarding resource scoping part. Please let me know if I misunderstood something.
| func (s *ContainerdSession) ContainersClient() containersapi.ContainersClient { | ||
| return containersapi.NewContainersClient(s.conn) | ||
| } |
There was a problem hiding this comment.
This may not need to be exported, since it is currently only used internally by loadContainer()
| // ImagesClient returns a generated images service client scoped to the session | ||
| // connection. | ||
| func (s *ContainerdSession) ImagesClient() imagesapi.ImagesClient { | ||
| return imagesapi.NewImagesClient(s.conn) | ||
| } | ||
|
|
||
| // ContentClient returns a generated content service client scoped to the | ||
| // session connection. | ||
| func (s *ContainerdSession) ContentClient() contentapi.ContentClient { | ||
| return contentapi.NewContentClient(s.conn) | ||
| } | ||
|
|
||
| // SnapshotsClient returns a generated snapshots service client scoped to the | ||
| // session connection. | ||
| func (s *ContainerdSession) SnapshotsClient() snapshotsapi.SnapshotsClient { | ||
| return snapshotsapi.NewSnapshotsClient(s.conn) | ||
| } | ||
|
|
||
| // LeasesClient returns a generated leases service client scoped to the session | ||
| // connection. | ||
| func (s *ContainerdSession) LeasesClient() leasesapi.LeasesClient { | ||
| return leasesapi.NewLeasesClient(s.conn) | ||
| } |
There was a problem hiding this comment.
In the issue discussion, my understanding was that each feature should only access the containerd resources it actually needs. For example, the annotation path should not need access to the snapshotter/lease clients, and the snapshot-view path should not need access to image/content clients unless required. This seems slightly different from the direction of limiting resource access per feature.
There was a problem hiding this comment.
I am not sure if this is ideal, but we could do something like this while keeping these client constructors private.
WDYT @sidneychang @cmainas
type annotationResources struct {
container *containersapi.Container
images imagesapi.ImagesClient
content contentapi.ContentClient
}
func newAnnotationResources(s *ContainerdSession) annotationResources {
return annotationResources{
container: s.Container(),
images: s.imagesClient(),
content: s.contentClient(),
}
}There was a problem hiding this comment.
I agree with @jiwahn . We do not need to expose these functions and they can be called whenever required. In any case, it is just a stub, no reason to expose it.
cmainas
left a comment
There was a problem hiding this comment.
Hello @sidneychang , I have added some more comments.
Regarding the manager, I would suggest we keep it separately. The manager and the task have different semantics.
| return fmt.Errorf("get container %q: empty response", s.containerID) | ||
| } | ||
|
|
||
| s.container = resp.GetContainer() |
There was a problem hiding this comment.
Why do we need a double call to GetContainer? It has already been called and we can store the return value in a variable.
|
|
||
| if err := session.loadContainer(ctx); err != nil { | ||
| if closeErr := conn.Close(); closeErr != nil { | ||
| return nil, fmt.Errorf("%w; close containerd connection: %v", err, closeErr) |
There was a problem hiding this comment.
The first error here will not be very clear. Let's add a few words before the error., like loadContainer failed: %w.
| return s.container | ||
| } | ||
|
|
||
| func (s *ContainerdSession) loadContainer(ctx context.Context) error { |
There was a problem hiding this comment.
Since getting the container takes place once, we should not add it as a method for ContainerdSession, but just a helper function.
| } | ||
|
|
||
| // Namespace returns the containerd namespace bound to this session. | ||
| func (s *ContainerdSession) Namespace() string { |
There was a problem hiding this comment.
Function name should be GetNamespace()
| } | ||
|
|
||
| // ContainerID returns the container ID bound to this session. | ||
| func (s *ContainerdSession) ContainerID() string { |
There was a problem hiding this comment.
Function name should be GetContainerID()
| } | ||
|
|
||
| // Namespace returns the containerd namespace bound to this session. | ||
| func (s *ContainerdSession) Namespace() string { | ||
| return s.namespace | ||
| } | ||
|
|
||
| // ContainerID returns the container ID bound to this session. | ||
| func (s *ContainerdSession) ContainerID() string { | ||
| return s.containerID | ||
| } | ||
|
|
||
| // Container returns the task-level container metadata loaded during session | ||
| // creation. | ||
| func (s *ContainerdSession) Container() *containersapi.Container { |
There was a problem hiding this comment.
Function name should be GetContainer()
| return nil | ||
| } | ||
|
|
||
| func (s *ContainerdSession) withNamespace(ctx context.Context) context.Context { |
There was a problem hiding this comment.
This should be a helper function, no need to get access to ContainerdSession, since namespace is just a string.
| func (s *ContainerdSession) ContainersClient() containersapi.ContainersClient { | ||
| return containersapi.NewContainersClient(s.conn) | ||
| } |
a888c83 to
457e776
Compare
|
Hello @jiwahn @cmainas, Thanks for the reviews. I initially exported the generated containerd client getters to avoid unused-code lint issues while the feature logic was not wired yet. Based on your feedback, I refined this so the constructors are private and the feature-specific resource structs expose only the clients each path needs. Could you please take another look? |
3c95b3d to
3e3c0dc
Compare
|
Thanks, Sidney. This looks good to me. How about adding a short comment above OpenSession? It opens a containerd session and also loads the container metadata. Other than that, I think this is good to go. |
3e3c0dc to
7e843ed
Compare
|
Thanks, @jiwahn. I have added the comment above OpenSession to clarify that it opens the containerd session and loads the container metadata. |
cmainas
left a comment
There was a problem hiding this comment.
Hey all, I think there has been some misunderstanding.
We are trying to establish a common interface to use the shared containerd resources required form the annotations and the snapshots PRs later. We decided to only share the common parts and let the respective additions (annotations / snapshots) to retrieve whatever is necessary for them. I thought this will be the role of the Session struct. Based on the current implementation (BaseAccess), I am missing how the annotations / snapshots parts will have access to the containerd connection, which is not exposed to any of them.
I do not really understand the role of BaseAccess. Sure, we do not want to expose everything in each feature, but the containerd connection is necessary since in the snapshot part we instruct containerd to create a view snapshot.
Moreveor, why are we defining the AnnotationAccess and the SnapshotAccess in this PR? The upcoming PRs should be responsible to set and manage the data structures they require.
| type BaseAccess struct { | ||
| namespace string | ||
| containerID string | ||
| container *containersapi.Container | ||
| } | ||
|
|
||
| func newBaseAccess(s *Session) BaseAccess { | ||
| return BaseAccess{ | ||
| namespace: s.namespace, | ||
| containerID: s.containerID, | ||
| container: s.container, | ||
| } | ||
| } |
| type AnnotationAccess struct { | ||
| BaseAccess | ||
|
|
||
| images imagesapi.ImagesClient | ||
| content contentapi.ContentClient | ||
| } | ||
|
|
||
| func NewAnnotationAccess(s *Session) AnnotationAccess { | ||
| return AnnotationAccess{ | ||
| BaseAccess: newBaseAccess(s), | ||
| images: s.imagesClient(), | ||
| content: s.contentClient(), | ||
| } | ||
| } | ||
|
|
||
| func (a AnnotationAccess) Images() imagesapi.ImagesClient { | ||
| return a.images | ||
| } | ||
|
|
||
| func (a AnnotationAccess) Content() contentapi.ContentClient { | ||
| return a.content | ||
| } |
There was a problem hiding this comment.
Why defining in this PR the annotations parts?
| type SnapshotViewAccess struct { | ||
| BaseAccess | ||
|
|
||
| snapshots snapshotsapi.SnapshotsClient | ||
| leases leasesapi.LeasesClient | ||
| } | ||
|
|
||
| func NewSnapshotViewAccess(s *Session) SnapshotViewAccess { | ||
| return SnapshotViewAccess{ | ||
| BaseAccess: newBaseAccess(s), | ||
| snapshots: s.snapshotsClient(), | ||
| leases: s.leasesClient(), | ||
| } | ||
| } | ||
|
|
||
| func (a SnapshotViewAccess) Snapshots() snapshotsapi.SnapshotsClient { | ||
| return a.snapshots | ||
| } | ||
|
|
||
| func (a SnapshotViewAccess) Leases() leasesapi.LeasesClient { | ||
| return a.leases | ||
| } |
There was a problem hiding this comment.
Why defining in this PR the snapshot parts?
36cb243 to
ab565d0
Compare
|
Sorry for the confusion, that was my mistake. When I was thinking through how to avoid exposing the raw containerd connection directly to future feature-specific paths, I considered an access-type structure that was somewhat similar to inheritance / polymorphism. After reconsidering it, I realized that this was not the right abstraction here. During that experiment, I accidentally included some extra code in this PR. I have fixed this by removing those access types. |
|
Thank you @sidneychang , if @jiwahn is also ok, we can rebase over main to merge. |
ab565d0 to
838e1b8
Compare
|
Looks good to me too. |
62855e5 to
4bcd59f
Compare
cmainas
left a comment
There was a problem hiding this comment.
Thank you @sidneychang for the addition and @jiwahn for the review.
PR: #657 Signed-off-by: sidneychang <2190206983@qq.com> Reviewed-by: Charalampos Mainas <cmainas@nubificus.co.uk> Reviewed-by: Jiwoo Ahn <ikwydls1314@gmail.com> Approved-by: Charalampos Mainas <cmainas@nubificus.co.uk>
PR: #657 Signed-off-by: sidneychang <2190206983@qq.com> Reviewed-by: Charalampos Mainas <cmainas@nubificus.co.uk> Reviewed-by: Jiwoo Ahn <ikwydls1314@gmail.com> Approved-by: Charalampos Mainas <cmainas@nubificus.co.uk>
PR: #657 Signed-off-by: sidneychang <2190206983@qq.com> Reviewed-by: Charalampos Mainas <cmainas@nubificus.co.uk> Reviewed-by: Jiwoo Ahn <ikwydls1314@gmail.com> Approved-by: Charalampos Mainas <cmainas@nubificus.co.uk>
PR: #657 Signed-off-by: sidneychang <2190206983@qq.com> Reviewed-by: Charalampos Mainas <cmainas@nubificus.co.uk> Reviewed-by: Jiwoo Ahn <ikwydls1314@gmail.com> Approved-by: Charalampos Mainas <cmainas@nubificus.co.uk>
Signed-off-by: sidneychang <2190206983@qq.com>
Signed-off-by: sidneychang <2190206983@qq.com>
Description
The containerd access layer adds a narrow
Sessionhelper that owns ashort-lived gRPC connection, namespace propagation, task-level container
metadata lookup, and gRPC error conversion. It keeps the raw connection private
and exposes only common task/container metadata through getters.
The generated service-client constructors are package-private helpers. They are
not exposed to feature code directly; later feature PRs can use them inside the
containerd access package to build narrow annotation or snapshot/view
access/resource constructors for only the clients each feature needs.
Related issues
How was this tested?
LLM usage
GPT-5
Checklist
make lint).make test_ctr,make test_nerdctl,make test_docker,make test_crictl).