Jean Pierre 06e02e4a9f
Enable vscode 1.85 for older linux distros (#19391)
* Enable vscode for older linux distros

* 💄

* Update code to 1.86

* Update code to 1.86
2024-02-05 11:24:47 +02:00

557 lines
16 KiB
Go

// 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 server
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"sync"
"time"
"github.com/containerd/containerd/remotes"
"github.com/containerd/containerd/remotes/docker"
"github.com/docker/cli/cli/config/configfile"
"github.com/gitpod-io/gitpod/common-go/baseserver"
"github.com/gitpod-io/gitpod/common-go/experiments"
"github.com/gitpod-io/gitpod/common-go/log"
"github.com/gitpod-io/gitpod/common-go/watch"
gitpodapi "github.com/gitpod-io/gitpod/gitpod-protocol"
api "github.com/gitpod-io/gitpod/ide-service-api"
"github.com/gitpod-io/gitpod/ide-service-api/config"
"github.com/heptiolabs/healthcheck"
"github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/health/grpc_health_v1"
)
// ResolverProvider provides new resolver
type ResolverProvider func() remotes.Resolver
type IDEServiceServer struct {
config *config.ServiceConfiguration
originIDEConfig []byte
parsedIDEConfigContent string
parsedCode1_85IDEConfigContent string
ideConfig *config.IDEConfig
code1_85IdeConfig *config.IDEConfig
ideConfigFileName string
experimentsClient experiments.Client
resolver ResolverProvider
api.UnimplementedIDEServiceServer
}
func Start(logger *logrus.Entry, cfg *config.ServiceConfiguration) error {
ctx := context.Background()
logger.WithField("config", cfg).Info("Starting ide-service.")
registry := prometheus.NewRegistry()
health := healthcheck.NewHandler()
srv, err := baseserver.New("ide-service",
baseserver.WithLogger(logger),
baseserver.WithConfig(cfg.Server),
baseserver.WithMetricsRegistry(registry),
baseserver.WithHealthHandler(health),
)
if err != nil {
return fmt.Errorf("failed to initialize ide-service: %w", err)
}
var (
dockerCfg *configfile.ConfigFile
dockerCfgMu sync.RWMutex
)
resolverProvider := func() remotes.Resolver {
var resolverOpts docker.ResolverOptions
dockerCfgMu.RLock()
defer dockerCfgMu.RUnlock()
if dockerCfg != nil {
resolverOpts.Hosts = docker.ConfigureDefaultRegistries(
docker.WithAuthorizer(authorizerFromDockerConfig(dockerCfg)),
)
}
return docker.NewResolver(resolverOpts)
}
if cfg.DockerCfg != "" {
dockerCfg = loadDockerCfg(cfg.DockerCfg)
err = watch.File(ctx, cfg.DockerCfg, func() {
dockerCfgMu.Lock()
defer dockerCfgMu.Unlock()
dockerCfg = loadDockerCfg(cfg.DockerCfg)
})
if err != nil {
log.WithError(err).Fatal("cannot start watch of Docker auth configuration file")
}
}
s := New(cfg, resolverProvider)
go s.watchIDEConfig(ctx)
go s.scheduleUpdate(ctx)
s.register(srv.GRPC())
health.AddReadinessCheck("ide-service", func() error {
if s.ideConfig == nil {
return fmt.Errorf("ide config is not ready")
}
return nil
})
health.AddReadinessCheck("grpc-server", grpcProbe(*cfg.Server.Services.GRPC))
if err := srv.ListenAndServe(); err != nil {
return fmt.Errorf("failed to serve ide-service: %w", err)
}
return nil
}
func loadDockerCfg(fn string) *configfile.ConfigFile {
if tproot := os.Getenv("TELEPRESENCE_ROOT"); tproot != "" {
fn = filepath.Join(tproot, fn)
}
fr, err := os.OpenFile(fn, os.O_RDONLY, 0)
if err != nil {
log.WithError(err).Fatal("cannot read docker auth config")
}
dockerCfg := configfile.New(fn)
err = dockerCfg.LoadFromReader(fr)
fr.Close()
if err != nil {
log.WithError(err).Fatal("cannot read docker config")
}
log.WithField("fn", fn).Info("using authentication for backing registries")
return dockerCfg
}
// FromDockerConfig turns docker client config into docker registry hosts
func authorizerFromDockerConfig(cfg *configfile.ConfigFile) docker.Authorizer {
return docker.NewDockerAuthorizer(docker.WithAuthCreds(func(host string) (user, pass string, err error) {
auth, err := cfg.GetAuthConfig(host)
if err != nil {
return
}
user = auth.Username
pass = auth.Password
return
}))
}
func New(cfg *config.ServiceConfiguration, resolver ResolverProvider) *IDEServiceServer {
fn, err := filepath.Abs(cfg.IDEConfigPath)
if err != nil {
log.WithField("path", cfg.IDEConfigPath).WithError(err).Fatal("cannot convert ide config path to abs path")
}
s := &IDEServiceServer{
config: cfg,
ideConfigFileName: fn,
experimentsClient: experiments.NewClient(),
resolver: resolver,
}
return s
}
func (s *IDEServiceServer) register(grpcServer *grpc.Server) {
api.RegisterIDEServiceServer(grpcServer, s)
}
func (s *IDEServiceServer) GetConfig(ctx context.Context, req *api.GetConfigRequest) (*api.GetConfigResponse, error) {
attributes := experiments.Attributes{
UserID: req.User.Id,
UserEmail: req.User.GetEmail(),
}
// Check flag to enable vscode for older linux distros (distros that don't support glibc 2.28)
enableVscodeForOlderLinuxDistros := s.experimentsClient.GetBoolValue(ctx, "enableVscodeForOlderLinuxDistros", false, attributes)
if enableVscodeForOlderLinuxDistros {
return &api.GetConfigResponse{
Content: s.parsedCode1_85IDEConfigContent,
}, nil
} else {
return &api.GetConfigResponse{
Content: s.parsedIDEConfigContent,
}, nil
}
}
func (s *IDEServiceServer) readIDEConfig(ctx context.Context, isInit bool) {
ideConfigbuffer, err := os.ReadFile(s.ideConfigFileName)
if err != nil {
log.WithError(err).Warn("cannot read original ide config file")
return
}
if originalIdeConfig, err := ParseConfig(ctx, s.resolver(), ideConfigbuffer); err != nil {
if !isInit {
log.WithError(err).Fatal("cannot parse original ide config")
}
log.WithError(err).Error("cannot parse original ide config")
return
} else {
parsedConfig, err := json.Marshal(originalIdeConfig)
if err != nil {
log.WithError(err).Error("cannot marshal original ide config")
return
}
// Precompute the config without code 1.85
code1_85IdeOptions := originalIdeConfig.IdeOptions.Options
ideOptions := make(map[string]config.IDEOption)
for key, ide := range code1_85IdeOptions {
if key != "code1_85" {
ideOptions[key] = ide
}
}
ideConfig := &config.IDEConfig{
SupervisorImage: originalIdeConfig.SupervisorImage,
IdeOptions: config.IDEOptions{
Options: ideOptions,
DefaultIde: originalIdeConfig.IdeOptions.DefaultIde,
DefaultDesktopIde: originalIdeConfig.IdeOptions.DefaultDesktopIde,
Clients: originalIdeConfig.IdeOptions.Clients,
},
}
parsedIdeConfig, err := json.Marshal(ideConfig)
if err != nil {
log.WithError(err).Error("cannot marshal ide config")
return
}
s.parsedCode1_85IDEConfigContent = string(parsedConfig)
s.code1_85IdeConfig = originalIdeConfig
s.ideConfig = ideConfig
s.parsedIDEConfigContent = string(parsedIdeConfig)
s.originIDEConfig = ideConfigbuffer
log.Info("ide config updated")
}
}
func (s *IDEServiceServer) watchIDEConfig(ctx context.Context) {
go s.readIDEConfig(ctx, true)
// `watch.File` only watch for create and remove event
// so with locally debugging, we cannot watch example ide config file change
// but in k8s, configmap change will create/remove file to replace it
if err := watch.File(ctx, s.ideConfigFileName, func() {
s.readIDEConfig(ctx, false)
}); err != nil {
log.WithError(err).Fatal("cannot start watch of ide config file")
}
}
func (s *IDEServiceServer) scheduleUpdate(ctx context.Context) {
t := time.NewTicker(time.Hour * 1)
for {
select {
case <-t.C:
log.Info("schedule update config")
s.readIDEConfig(ctx, false)
case <-ctx.Done():
t.Stop()
return
}
}
}
func grpcProbe(cfg baseserver.ServerConfiguration) func() error {
return func() error {
creds := insecure.NewCredentials()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
conn, err := grpc.DialContext(ctx, cfg.Address, grpc.WithTransportCredentials(creds))
if err != nil {
return err
}
defer conn.Close()
client := grpc_health_v1.NewHealthClient(conn)
check, err := client.Check(ctx, &grpc_health_v1.HealthCheckRequest{})
if err != nil {
return err
}
if check.Status == grpc_health_v1.HealthCheckResponse_SERVING {
return nil
}
return fmt.Errorf("grpc service not ready")
}
}
type IDESettings struct {
DefaultIde string `json:"defaultIde,omitempty"`
UseLatestVersion bool `json:"useLatestVersion,omitempty"`
}
type WorkspaceContext struct {
Referrer string `json:"referrer,omitempty"`
ReferrerIde string `json:"referrerIde,omitempty"`
}
func (s *IDEServiceServer) resolveReferrerIDE(ideConfig *config.IDEConfig, wsCtx *WorkspaceContext, chosenIDEName string) (ideName string, ideOption *config.IDEOption) {
if wsCtx == nil || wsCtx.Referrer == "" {
return
}
client, ok := ideConfig.IdeOptions.Clients[wsCtx.Referrer]
if !ok {
return
}
getValidIDEOption := func(ideName string) (*config.IDEOption, bool) {
optionToCheck, ok := ideConfig.IdeOptions.Options[ideName]
if !ok {
return nil, false
}
for _, ide := range client.DesktopIDEs {
if ide == ideName {
return &optionToCheck, true
}
}
return nil, false
}
ideOption, ok = getValidIDEOption(wsCtx.ReferrerIde)
if ok {
ideName = wsCtx.ReferrerIde
return
}
ideOption, ok = getValidIDEOption(chosenIDEName)
if ok {
ideName = chosenIDEName
return
}
ideOption, ok = getValidIDEOption(client.DefaultDesktopIDE)
if ok {
ideName = client.DefaultDesktopIDE
return
}
return
}
func (s *IDEServiceServer) ResolveWorkspaceConfig(ctx context.Context, req *api.ResolveWorkspaceConfigRequest) (resp *api.ResolveWorkspaceConfigResponse, err error) {
log.WithField("req", req).Debug("receive ResolveWorkspaceConfig request")
// make a copy for ref ideConfig, it's safe because we replace ref in update config
ideConfig := s.code1_85IdeConfig
var defaultIde *config.IDEOption
if ide, ok := ideConfig.IdeOptions.Options[ideConfig.IdeOptions.DefaultIde]; !ok {
// I think it never happen, we have a check to make sure all DefaultIDE should be in Options
log.WithError(err).WithField("defaultIDE", ideConfig.IdeOptions.DefaultIde).Error("IDE configuration corrupt, cannot found defaultIDE")
return nil, fmt.Errorf("IDE configuration corrupt")
} else {
defaultIde = &ide
}
resp = &api.ResolveWorkspaceConfigResponse{
SupervisorImage: ideConfig.SupervisorImage,
WebImage: defaultIde.Image,
IdeImageLayers: defaultIde.ImageLayers,
}
if os.Getenv("CONFIGCAT_SDK_KEY") != "" {
resp.Envvars = append(resp.Envvars, &api.EnvironmentVariable{
Name: "GITPOD_CONFIGCAT_ENABLED",
Value: "true",
})
}
var wsConfig *gitpodapi.GitpodConfig
if req.WorkspaceConfig != "" {
if err := json.Unmarshal([]byte(req.WorkspaceConfig), &wsConfig); err != nil {
log.WithError(err).WithField("workspaceConfig", req.WorkspaceConfig).Error("failed to parse workspace config")
}
}
if req.Type == api.WorkspaceType_REGULAR {
var ideSettings *IDESettings
var wsContext *WorkspaceContext
if req.IdeSettings != "" {
if err := json.Unmarshal([]byte(req.IdeSettings), &ideSettings); err != nil {
log.WithError(err).WithField("ideSetting", req.IdeSettings).Error("failed to parse ide settings")
}
}
if req.Context != "" {
if err := json.Unmarshal([]byte(req.Context), &wsContext); err != nil {
log.WithError(err).WithField("context", req.Context).Error("failed to parse context")
}
}
userIdeName := ""
useLatest := false
resultingIdeName := ideConfig.IdeOptions.DefaultIde
if ideSettings != nil {
userIdeName = ideSettings.DefaultIde
useLatest = ideSettings.UseLatestVersion
}
chosenIDE := defaultIde
getUserIDEImage := func(ideOption *config.IDEOption) string {
if useLatest && ideOption.LatestImage != "" {
return ideOption.LatestImage
}
return ideOption.Image
}
getUserImageLayers := func(ideOption *config.IDEOption) []string {
if useLatest {
return ideOption.LatestImageLayers
}
return ideOption.ImageLayers
}
if userIdeName != "" {
if ide, ok := ideConfig.IdeOptions.Options[userIdeName]; ok {
chosenIDE = &ide
resultingIdeName = userIdeName
// TODO: Currently this variable reflects the IDE selected in
// user's settings for backward compatibility but in the future
// we want to make it represent the actual IDE.
ideAlias := api.EnvironmentVariable{
Name: "GITPOD_IDE_ALIAS",
Value: userIdeName,
}
resp.Envvars = append(resp.Envvars, &ideAlias)
}
}
// we always need WebImage for when the user chooses a desktop ide
resp.WebImage = getUserIDEImage(defaultIde)
resp.IdeImageLayers = getUserImageLayers(defaultIde)
var desktopImageLayer string
var desktopUserImageLayers []string
if chosenIDE.Type == config.IDETypeDesktop {
desktopImageLayer = getUserIDEImage(chosenIDE)
desktopUserImageLayers = getUserImageLayers(chosenIDE)
} else {
resp.WebImage = getUserIDEImage(chosenIDE)
resp.IdeImageLayers = getUserImageLayers(chosenIDE)
}
// TODO (se) this should be handled on the surface (i.e. server or even dashboard) and not passed as a special workspace context down here.
ideName, referrer := s.resolveReferrerIDE(ideConfig, wsContext, userIdeName)
if ideName != "" {
resp.RefererIde = ideName
resultingIdeName = ideName
desktopImageLayer = getUserIDEImage(referrer)
desktopUserImageLayers = getUserImageLayers(referrer)
}
if desktopImageLayer != "" {
resp.IdeImageLayers = append(resp.IdeImageLayers, desktopImageLayer)
resp.IdeImageLayers = append(resp.IdeImageLayers, desktopUserImageLayers...)
}
// we are returning the actually used ide name here, which might be different from the user's choice
ideSettingsEncoded := new(bytes.Buffer)
enc := json.NewEncoder(ideSettingsEncoded)
enc.SetEscapeHTML(false)
resultingIdeSettings := &IDESettings{
DefaultIde: resultingIdeName,
UseLatestVersion: useLatest,
}
err = enc.Encode(resultingIdeSettings)
if err != nil {
log.WithError(err).Error("cannot marshal ideSettings")
}
resp.IdeSettings = ideSettingsEncoded.String()
}
// TODO figure out how to make it configurable on IDE level, not hardcoded here
jbGW, ok := ideConfig.IdeOptions.Clients["jetbrains-gateway"]
if req.Type == api.WorkspaceType_PREBUILD && ok {
imageLayers := make(map[string]struct{})
for _, alias := range jbGW.DesktopIDEs {
prebuilds := getPrebuilds(wsConfig, alias)
if prebuilds != nil {
if prebuilds.Version != "latest" {
if ide, ok := ideConfig.IdeOptions.Options[alias]; ok {
for _, ideImageLayer := range ide.ImageLayers {
if _, ok := imageLayers[ideImageLayer]; !ok {
imageLayers[ideImageLayer] = struct{}{}
resp.IdeImageLayers = append(resp.IdeImageLayers, ideImageLayer)
}
}
resp.IdeImageLayers = append(resp.IdeImageLayers, ide.Image)
}
}
if prebuilds.Version != "stable" {
if ide, ok := ideConfig.IdeOptions.Options[alias]; ok {
for _, latestIdeImageLayer := range ide.LatestImageLayers {
if _, ok := imageLayers[latestIdeImageLayer]; !ok {
imageLayers[latestIdeImageLayer] = struct{}{}
resp.IdeImageLayers = append(resp.IdeImageLayers, latestIdeImageLayer)
}
}
resp.IdeImageLayers = append(resp.IdeImageLayers, ide.LatestImage)
}
}
}
}
}
return
}
func getPrebuilds(config *gitpodapi.GitpodConfig, alias string) *gitpodapi.Prebuilds {
if config == nil || config.Jetbrains == nil {
return nil
}
productConfig := getProductConfig(config, alias)
if productConfig == nil {
return nil
}
return productConfig.Prebuilds
}
func getProductConfig(config *gitpodapi.GitpodConfig, alias string) *gitpodapi.JetbrainsProduct {
defer func() {
if err := recover(); err != nil {
log.WithField("error", err).WithField("alias", alias).Error("failed to extract JB product config")
}
}()
v := reflect.ValueOf(*config.Jetbrains).FieldByNameFunc(func(s string) bool {
return strings.ToLower(s) == alias
}).Interface()
productConfig, ok := v.(*gitpodapi.JetbrainsProduct)
if !ok {
return nil
}
return productConfig
}