Skip to content

Commit

Permalink
Get more insights on workspace image (#20356)
Browse files Browse the repository at this point in the history
* 1

* 1

* 1

* add timeout ctx

* addressed feedback
  • Loading branch information
iQQBot authored Nov 12, 2024
1 parent f7a95c2 commit 66a1ba0
Show file tree
Hide file tree
Showing 18 changed files with 274 additions and 107 deletions.
11 changes: 11 additions & 0 deletions components/registry-facade/pkg/registry/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"io"
"mime"
"net/http"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -185,13 +186,23 @@ func (mh *manifestHandler) getManifest(w http.ResponseWriter, r *http.Request) {
return err
}

originImageSize := 0
for _, layer := range manifest.Layers {
originImageSize += int(layer.Size)
}

// modify config
addonLayer, err := mh.ConfigModifier(ctx, mh.Spec, cfg)
if err != nil {
log.WithError(err).WithFields(logFields).Error("cannot modify config")
return err
}
manifest.Layers = append(manifest.Layers, addonLayer...)
if manifest.Annotations == nil {
manifest.Annotations = make(map[string]string)
}
manifest.Annotations["io.gitpod.workspace-image.size"] = strconv.Itoa(originImageSize)
manifest.Annotations["io.gitpod.workspace-image.ref"] = mh.Spec.BaseRef

// place config in store
rawCfg, err := json.Marshal(cfg)
Expand Down
3 changes: 2 additions & 1 deletion components/ws-daemon/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ require (
github.com/containerd/continuity v0.4.2 // indirect
github.com/containerd/fifo v1.1.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/containerd/ttrpc v1.2.2 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
Expand Down Expand Up @@ -144,7 +145,7 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
github.com/opencontainers/selinux v1.11.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions components/ws-daemon/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions components/ws-daemon/pkg/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"context"

"golang.org/x/xerrors"

workspacev1 "github.com/gitpod-io/gitpod/ws-manager/api/crd/v1"
)

// Runtime abstracts over the different container runtimes out there w.r.t. to the features we need from those runtimes
Expand Down Expand Up @@ -47,6 +49,8 @@ type Runtime interface {

// IsContainerdReady returns is the status of containerd.
IsContainerdReady(ctx context.Context) (bool, error)

GetContainerImageInfo(ctx context.Context, id ID) (*workspacev1.WorkspaceImageInfo, error)
}

var (
Expand Down
40 changes: 40 additions & 0 deletions components/ws-daemon/pkg/container/containerd.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"fmt"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
Expand All @@ -20,6 +21,8 @@ import (
"github.com/containerd/containerd/api/types"
"github.com/containerd/containerd/containers"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images"
"github.com/containerd/platforms"
"github.com/containerd/typeurl/v2"
ocispecs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/opentracing/opentracing-go"
Expand All @@ -28,6 +31,7 @@ import (
wsk8s "github.com/gitpod-io/gitpod/common-go/kubernetes"
"github.com/gitpod-io/gitpod/common-go/log"
"github.com/gitpod-io/gitpod/common-go/tracing"
workspacev1 "github.com/gitpod-io/gitpod/ws-manager/api/crd/v1"
)

const (
Expand Down Expand Up @@ -92,6 +96,7 @@ type containerInfo struct {
UpperDir string
CGroupPath string
PID uint32
ImageRef string
}

// start listening to containerd
Expand Down Expand Up @@ -276,6 +281,7 @@ func (s *Containerd) handleNewContainer(c containers.Container) {
info.ID = c.ID
info.SnapshotKey = c.SnapshotKey
info.Snapshotter = c.Snapshotter
info.ImageRef = c.Image

s.cntIdx[c.ID] = info
log.WithField("podname", podName).WithFields(log.OWI(info.OwnerID, info.WorkspaceID, info.InstanceID)).WithField("ID", c.ID).Debug("found workspace container - updating label cache")
Expand Down Expand Up @@ -480,6 +486,40 @@ func (s *Containerd) ContainerPID(ctx context.Context, id ID) (pid uint64, err e
return uint64(info.PID), nil
}

func (s *Containerd) GetContainerImageInfo(ctx context.Context, id ID) (*workspacev1.WorkspaceImageInfo, error) {
info, ok := s.cntIdx[string(id)]
if !ok {
return nil, ErrNotFound
}

image, err := s.Client.GetImage(ctx, info.ImageRef)
if err != nil {
return nil, err
}
size, err := image.Size(ctx)
if err != nil {
return nil, err
}

wsImageInfo := &workspacev1.WorkspaceImageInfo{
TotalSize: size,
}

// Fetch the manifest
manifest, err := images.Manifest(ctx, s.Client.ContentStore(), image.Target(), platforms.Default())
if err != nil {
log.WithError(err).WithField("image", info.ImageRef).Error("Failed to get manifest")
return wsImageInfo, nil
}
if manifest.Annotations != nil {
wsImageInfo.WorkspaceImageRef = manifest.Annotations["io.gitpod.workspace-image.ref"]
if size, err := strconv.Atoi(manifest.Annotations["io.gitpod.workspace-image.size"]); err == nil {
wsImageInfo.WorkspaceImageSize = int64(size)
}
}
return wsImageInfo, nil
}

func (s *Containerd) IsContainerdReady(ctx context.Context) (bool, error) {
if len(s.registryFacadeHost) == 0 {
return s.Client.IsServing(ctx)
Expand Down
11 changes: 6 additions & 5 deletions components/ws-daemon/pkg/controller/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion components/ws-daemon/pkg/controller/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ var _ = BeforeSuite(func() {
Expect(err).ToNot(HaveOccurred())
ctx, cancel = context.WithCancel(context.Background())

workspaceCtrl, err = NewWorkspaceController(k8sClient, record.NewFakeRecorder(100), NodeName, secretsNamespace, 5, nil, ctrl_metrics.Registry)
workspaceCtrl, err = NewWorkspaceController(k8sClient, record.NewFakeRecorder(100), NodeName, secretsNamespace, 5, nil, ctrl_metrics.Registry, nil)
Expect(err).NotTo(HaveOccurred())

Expect(workspaceCtrl.SetupWithManager(k8sManager)).To(Succeed())
Expand Down
39 changes: 37 additions & 2 deletions components/ws-daemon/pkg/controller/workspace_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,10 @@ type WorkspaceController struct {
metrics *workspaceMetrics
secretNamespace string
recorder record.EventRecorder
runtime container.Runtime
}

func NewWorkspaceController(c client.Client, recorder record.EventRecorder, nodeName, secretNamespace string, maxConcurrentReconciles int, ops WorkspaceOperations, reg prometheus.Registerer) (*WorkspaceController, error) {
func NewWorkspaceController(c client.Client, recorder record.EventRecorder, nodeName, secretNamespace string, maxConcurrentReconciles int, ops WorkspaceOperations, reg prometheus.Registerer, runtime container.Runtime) (*WorkspaceController, error) {
metrics := newWorkspaceMetrics()
reg.Register(metrics)

Expand All @@ -75,6 +76,7 @@ func NewWorkspaceController(c client.Client, recorder record.EventRecorder, node
metrics: metrics,
secretNamespace: secretNamespace,
recorder: recorder,
runtime: runtime,
}, nil
}

Expand Down Expand Up @@ -219,7 +221,40 @@ func (wsc *WorkspaceController) handleWorkspaceRunning(ctx context.Context, ws *
span, ctx := opentracing.StartSpanFromContext(ctx, "handleWorkspaceRunning")
defer tracing.FinishSpan(span, &err)

return ctrl.Result{}, wsc.operations.SetupWorkspace(ctx, ws.Name)
var imageInfo *workspacev1.WorkspaceImageInfo = nil
if ws.Status.ImageInfo == nil {
getImageInfo := func() (*workspacev1.WorkspaceImageInfo, error) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
id, err := wsc.runtime.WaitForContainer(ctx, ws.Name)
if err != nil {
return nil, fmt.Errorf("failed to wait for container: %w", err)
}
info, err := wsc.runtime.GetContainerImageInfo(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get container image info: %w", err)
}

err = retry.RetryOnConflict(retryParams, func() error {
if err := wsc.Get(ctx, req.NamespacedName, ws); err != nil {
return err
}
ws.Status.ImageInfo = info
return wsc.Status().Update(ctx, ws)
})
if err != nil {
return info, fmt.Errorf("failed to update workspace with image info: %w", err)
}
return info, nil
}
imageInfo, err = getImageInfo()
if err != nil {
glog.WithFields(ws.OWI()).WithField("workspace", req.NamespacedName).Errorf("failed to get image info: %v", err)
} else {
glog.WithFields(ws.OWI()).WithField("workspace", req.NamespacedName).WithField("imageInfo", glog.TrustedValueWrap{Value: imageInfo}).Info("updated image info")
}
}
return ctrl.Result{}, wsc.operations.SetupWorkspace(ctx, ws.Name, imageInfo)
}

func (wsc *WorkspaceController) handleWorkspaceStop(ctx context.Context, ws *workspacev1.Workspace, req ctrl.Request) (result ctrl.Result, err error) {
Expand Down
33 changes: 29 additions & 4 deletions components/ws-daemon/pkg/controller/workspace_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package controller

import (
"context"
"encoding/json"
"errors"
"fmt"
"io/fs"
Expand All @@ -22,6 +23,7 @@ import (
"github.com/gitpod-io/gitpod/content-service/pkg/storage"
"github.com/gitpod-io/gitpod/ws-daemon/pkg/content"
"github.com/gitpod-io/gitpod/ws-daemon/pkg/internal/session"
workspacev1 "github.com/gitpod-io/gitpod/ws-manager/api/crd/v1"
"github.com/opentracing/opentracing-go"
"github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -71,7 +73,7 @@ type WorkspaceOperations interface {
// Snapshot takes a snapshot of the workspace
Snapshot(ctx context.Context, instanceID, snapshotName string) (err error)
// Setup ensures that the workspace has been setup
SetupWorkspace(ctx context.Context, instanceID string) error
SetupWorkspace(ctx context.Context, instanceID string, imageInfo *workspacev1.WorkspaceImageInfo) error
}

type DefaultWorkspaceOperations struct {
Expand Down Expand Up @@ -211,12 +213,15 @@ func (wso *DefaultWorkspaceOperations) creator(owner, workspaceID, instanceID st
}
}

func (wso *DefaultWorkspaceOperations) SetupWorkspace(ctx context.Context, instanceID string) error {
_, err := wso.provider.GetAndConnect(ctx, instanceID)
func (wso *DefaultWorkspaceOperations) SetupWorkspace(ctx context.Context, instanceID string, imageInfo *workspacev1.WorkspaceImageInfo) error {
ws, err := wso.provider.GetAndConnect(ctx, instanceID)
if err != nil {
return fmt.Errorf("cannot setup workspace %s: %w", instanceID, err)
}

err = wso.writeImageInfo(ctx, ws, imageInfo)
if err != nil {
glog.WithError(err).WithFields(ws.OWI()).Error("cannot write image info")
}
return nil
}

Expand Down Expand Up @@ -513,6 +518,26 @@ func (wso *DefaultWorkspaceOperations) uploadWorkspaceContent(ctx context.Contex
return nil
}

func (wso *DefaultWorkspaceOperations) writeImageInfo(_ context.Context, ws *session.Workspace, imageInfo *workspacev1.WorkspaceImageInfo) error {
if imageInfo == nil {
return nil
}

b, err := json.Marshal(imageInfo)
if err != nil {
return fmt.Errorf("cannot marshal image info: %w", err)
}
uid := (wsinit.GitpodUID + 100000 - 1)
gid := (wsinit.GitpodGID + 100000 - 1)
fp := filepath.Join(ws.Location, ".gitpod/image")
err = os.WriteFile(fp, b, 0644)
if err != nil {
return fmt.Errorf("cannot write image info: %w", err)
}
os.Chown(fp, uid, gid)
return nil
}

func retryIfErr(ctx context.Context, attempts int, log *logrus.Entry, op func(ctx context.Context) error) (err error) {
//nolint:ineffassign
span, ctx := opentracing.StartSpanFromContext(ctx, "retryIfErr")
Expand Down
2 changes: 1 addition & 1 deletion components/ws-daemon/pkg/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ func NewDaemon(config Config) (*Daemon, error) {
}

wsctrl, err := controller.NewWorkspaceController(
mgr.GetClient(), mgr.GetEventRecorderFor("workspace"), nodename, config.Runtime.SecretsNamespace, config.WorkspaceController.MaxConcurrentReconciles, workspaceOps, wrappedReg)
mgr.GetClient(), mgr.GetEventRecorderFor("workspace"), nodename, config.Runtime.SecretsNamespace, config.WorkspaceController.MaxConcurrentReconciles, workspaceOps, wrappedReg, containerRuntime)
if err != nil {
return nil, err
}
Expand Down
14 changes: 14 additions & 0 deletions components/ws-manager-api/go/crd/v1/workspace_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,17 @@ func (ps PortSpec) Equal(other PortSpec) bool {
return true
}

type WorkspaceImageInfo struct {
// +kubebuilder:validation:Required
TotalSize int64 `json:"totalSize"`

// +kubebuilder:validation:Optional
WorkspaceImageSize int64 `json:"workspaceImageSize,omitempty"`

// +kubebuilder:validation:Optional
WorkspaceImageRef string `json:"workspaceImageRef,omitempty"`
}

// WorkspaceStatus defines the observed state of Workspace
type WorkspaceStatus struct {
PodStarts int `json:"podStarts"`
Expand All @@ -193,6 +204,9 @@ type WorkspaceStatus struct {
Storage StorageStatus `json:"storage,omitempty"`

LastActivity *metav1.Time `json:"lastActivity,omitempty"`

// +kubebuilder:validation:Optional
ImageInfo *WorkspaceImageInfo `json:"imageInfo,omitempty"`
}

func (s *WorkspaceStatus) SetCondition(cond metav1.Condition) {
Expand Down
Loading

0 comments on commit 66a1ba0

Please sign in to comment.