Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get more insights on workspace image #20356

Merged
merged 5 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
geropl marked this conversation as resolved.
Show resolved Hide resolved
}

// 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
31 changes: 29 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,32 @@ 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 {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
id, err := wsc.runtime.WaitForContainer(ctx, ws.Name)
iQQBot marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
geropl marked this conversation as resolved.
Show resolved Hide resolved
return ctrl.Result{}, fmt.Errorf("failed to wait for container: %w", err)
}
info, err := wsc.runtime.GetContainerImageInfo(ctx, id)
if err != nil {
return ctrl.Result{}, 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 {
glog.WithFields(ws.OWI()).WithField("workspace", req.NamespacedName).WithField("phase", ws.Status.Phase).Errorf("failed to update workspace with image info: %v", err)
}
imageInfo = 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)
geropl marked this conversation as resolved.
Show resolved Hide resolved
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"`
geropl marked this conversation as resolved.
Show resolved Hide resolved

// +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
Loading