diff --git a/components/ide-service-api/go/config/ideconfig.go b/components/ide-service-api/go/config/ideconfig.go index d6df942de27b1b..4e491207e49cd1 100644 --- a/components/ide-service-api/go/config/ideconfig.go +++ b/components/ide-service-api/go/config/ideconfig.go @@ -55,6 +55,10 @@ type IDEOption struct { PluginImage string `json:"pluginImage,omitempty"` // PluginLatestImage ref for the latest IDE image, this image ref always resolve to digest. PluginLatestImage string `json:"pluginLatestImage,omitempty"` + // ImageVersion the semantic version of the IDE image. + ImageVersion string `json:"imageVersion,omitempty"` + // LatestImageVersion the semantic version of the latest IDE image. + LatestImageVersion string `json:"latestImageVersion,omitempty"` } type IDEClient struct { diff --git a/components/ide-service/go.mod b/components/ide-service/go.mod index 44ff69ac23670d..00c50eda269c52 100644 --- a/components/ide-service/go.mod +++ b/components/ide-service/go.mod @@ -9,6 +9,8 @@ require ( github.com/gitpod-io/gitpod/gitpod-protocol v0.0.0-00010101000000-000000000000 github.com/gitpod-io/gitpod/ide-service-api v0.0.0-00010101000000-000000000000 github.com/heptiolabs/healthcheck v0.0.0-20211123025425-613501dd5deb + github.com/opencontainers/go-digest v1.0.0 + github.com/opencontainers/image-spec v1.0.1 github.com/prometheus/client_golang v1.13.0 github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.4.0 @@ -34,8 +36,6 @@ require ( github.com/klauspost/compress v1.13.6 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/moby/locker v1.0.1 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.1 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/components/ide-service/pkg/ocitool/resolve.go b/components/ide-service/pkg/ocitool/resolve.go index 687801fc2f5060..93206862fdc7e8 100644 --- a/components/ide-service/pkg/ocitool/resolve.go +++ b/components/ide-service/pkg/ocitool/resolve.go @@ -6,10 +6,15 @@ package oci_tool import ( "context" + "encoding/json" + "fmt" + "io/ioutil" "time" + "github.com/containerd/containerd/remotes" "github.com/containerd/containerd/remotes/docker" "github.com/docker/distribution/reference" + ociv1 "github.com/opencontainers/image-spec/specs-go/v1" ) func Resolve(ctx context.Context, ref string) (string, error) { @@ -32,3 +37,74 @@ func Resolve(ctx context.Context, ref string) (string, error) { } return cref.String(), nil } + +func interactiveFetchManifestOrIndex(ctx context.Context, res remotes.Resolver, ref string) (name string, result *ociv1.Manifest, err error) { + resolved, desc, err := res.Resolve(ctx, ref) + if err != nil { + return "", nil, fmt.Errorf("cannot resolve %v: %w", ref, err) + } + + fetcher, err := res.Fetcher(ctx, resolved) + if err != nil { + return "", nil, err + } + + in, err := fetcher.Fetch(ctx, desc) + if err != nil { + return "", nil, err + } + defer in.Close() + buf, err := ioutil.ReadAll(in) + if err != nil { + return "", nil, err + } + + var mf ociv1.Manifest + err = json.Unmarshal(buf, &mf) + if err != nil { + return "", nil, fmt.Errorf("cannot unmarshal manifest: %w", err) + } + + if mf.Config.Size != 0 { + return resolved, &mf, nil + } + return "", nil, nil +} + +func ResolveIDEVersion(ctx context.Context, ref string) (string, error) { + newCtx, cancel := context.WithTimeout(ctx, time.Second*30) + defer cancel() + res := docker.NewResolver(docker.ResolverOptions{}) + + name, mf, err := interactiveFetchManifestOrIndex(newCtx, res, ref) + if err != nil { + return "", err + } + + fetcher, err := res.Fetcher(ctx, name) + if err != nil { + return "", err + } + + cfgin, err := fetcher.Fetch(ctx, mf.Config) + if err != nil { + return "", err + } + defer cfgin.Close() + + var tmp ManifestJSON + + err = json.NewDecoder(cfgin).Decode(&tmp) + if err != nil { + return "", nil + } + return tmp.Config.Labels.Version, nil +} + +type ManifestJSON struct { + Config struct { + Labels struct { + Version string `json:"io.gitpod.ide.version"` + } `json:"Labels"` + } `json:"config"` +} diff --git a/components/ide-service/pkg/ocitool/resolve_test.go b/components/ide-service/pkg/ocitool/resolve_test.go new file mode 100644 index 00000000000000..44372ff7ec7a8f --- /dev/null +++ b/components/ide-service/pkg/ocitool/resolve_test.go @@ -0,0 +1,51 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package oci_tool + +import ( + "context" + "testing" +) + +func TestResolveIDEVersion(t *testing.T) { + type args struct { + ref string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "happy path", + args: args{ + ref: "eu.gcr.io/gitpod-core-dev/build/ide/goland:latest@sha256:06bf4d6fb7a55427f5e83e46ed9a2561930981ec044cf914276c0a92b45f5d30", + }, + want: "2022.3", + wantErr: false, + }, + { + name: "image for vscode desktop version not found", + args: args{ + ref: "eu.gcr.io/gitpod-core-dev/build/ide/code-desktop:commit-00c77a9d85e85f210b0e564119f7e9889d75317e", + }, + want: "", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ResolveIDEVersion(context.TODO(), tt.args.ref) + if (err != nil) != tt.wantErr { + t.Errorf("ResolveIDEVersion() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ResolveIDEVersion() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/components/ide-service/pkg/server/ideconfig.go b/components/ide-service/pkg/server/ideconfig.go index f3c5e4ed009172..af55411f0b1ca7 100644 --- a/components/ide-service/pkg/server/ideconfig.go +++ b/components/ide-service/pkg/server/ideconfig.go @@ -72,6 +72,11 @@ func ParseConfig(ctx context.Context, b []byte) (*config.IDEConfig, error) { option.Image = resolved } } + if resolvedVersion, err := oci_tool.ResolveIDEVersion(ctx, option.Image); err != nil { + log.WithError(err).Error("ide config: cannot get version from image") + } else { + option.ImageVersion = resolvedVersion + } if option.LatestImage != "" { if resolved, err := oci_tool.Resolve(ctx, option.LatestImage); err != nil { log.WithError(err).Error("ide config: cannot resolve latest image digest") @@ -79,6 +84,11 @@ func ParseConfig(ctx context.Context, b []byte) (*config.IDEConfig, error) { log.WithField("ide", id).WithField("image", option.LatestImage).WithField("resolved", resolved).Info("ide config: resolved latest image digest") option.LatestImage = resolved } + if resolvedVersion, err := oci_tool.ResolveIDEVersion(ctx, option.LatestImage); err != nil { + log.WithError(err).Error("ide config: cannot get version from image") + } else { + option.LatestImageVersion = resolvedVersion + } } cfg.IdeOptions.Options[id] = option }