gitpod/components/ws-proxy/pkg/proxy/infoprovider.go
Cornelius A. Ludmann 47d4ac5345 [ws-proxy] Get supervisor image from pod annotation
instead from ws-proxy config
2021-11-15 08:45:12 +01:00

218 lines
6.2 KiB
Go

// Copyright (c) 2020 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 proxy
import (
"context"
"net/url"
"strings"
"time"
"golang.org/x/xerrors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/cache"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/gitpod-io/gitpod/common-go/kubernetes"
"github.com/gitpod-io/gitpod/common-go/log"
regapi "github.com/gitpod-io/gitpod/registry-facade/api"
"github.com/gitpod-io/gitpod/ws-manager/api"
wsapi "github.com/gitpod-io/gitpod/ws-manager/api"
)
// WorkspaceCoords represents the coordinates of a workspace (port).
type WorkspaceCoords struct {
// The workspace ID
ID string
// The workspace port
Port string
}
// WorkspaceInfoProvider is an entity that is able to provide workspaces related information.
type WorkspaceInfoProvider interface {
// WorkspaceInfo returns the workspace information of a workspace using it's workspace ID
WorkspaceInfo(workspaceID string) *WorkspaceInfo
}
// WorkspaceInfo is all the infos ws-proxy needs to know about a workspace.
type WorkspaceInfo struct {
WorkspaceID string
InstanceID string
URL string
IDEImage string
SupervisorImage string
// (parsed from URL)
IDEPublicPort string
IPAddress string
Ports []*api.PortSpec
Auth *wsapi.WorkspaceAuthentication
StartedAt time.Time
}
// RemoteWorkspaceInfoProvider provides (cached) infos about running workspaces that it queries from ws-manager.
type RemoteWorkspaceInfoProvider struct {
client.Client
Scheme *runtime.Scheme
store cache.ThreadSafeStore
}
const (
workspaceIndex = "workspaceIndex"
)
// NewRemoteWorkspaceInfoProvider creates a fresh WorkspaceInfoProvider.
func NewRemoteWorkspaceInfoProvider(client client.Client, scheme *runtime.Scheme) *RemoteWorkspaceInfoProvider {
// create custom indexer for searches
indexers := cache.Indexers{
workspaceIndex: func(obj interface{}) ([]string, error) {
if workspaceInfo, ok := obj.(*WorkspaceInfo); ok {
return []string{workspaceInfo.WorkspaceID}, nil
}
return nil, xerrors.Errorf("object is not a WorkspaceInfo")
},
}
return &RemoteWorkspaceInfoProvider{
Client: client,
Scheme: scheme,
store: cache.NewThreadSafeStore(indexers, cache.Indices{}),
}
}
func (r *RemoteWorkspaceInfoProvider) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var pod corev1.Pod
err := r.Client.Get(context.Background(), req.NamespacedName, &pod)
if errors.IsNotFound(err) {
// pod is gone - that's ok
r.store.Delete(req.Name)
log.WithField("workspace", req.Name).Debug("removing workspace from store")
return reconcile.Result{}, nil
}
// extract workspace details from pod and store
workspaceInfo := mapPodToWorkspaceInfo(&pod)
r.store.Update(req.Name, workspaceInfo)
log.WithField("workspace", req.Name).WithField("details", workspaceInfo).Debug("adding/updating workspace details")
return ctrl.Result{}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *RemoteWorkspaceInfoProvider) SetupWithManager(mgr ctrl.Manager) error {
podWorkspaceSelector, err := predicate.LabelSelectorPredicate(metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "gitpod",
"component": "workspace",
},
})
if err != nil {
return err
}
return ctrl.NewControllerManagedBy(mgr).
Named("pod").
WithEventFilter(predicate.ResourceVersionChangedPredicate{}).
For(
&corev1.Pod{},
builder.WithPredicates(podWorkspaceSelector),
).
Complete(r)
}
func mapPodToWorkspaceInfo(pod *corev1.Pod) *WorkspaceInfo {
ownerToken := pod.Annotations[kubernetes.OwnerTokenAnnotation]
admission := wsapi.AdmissionLevel_ADMIT_OWNER_ONLY
if av, ok := wsapi.AdmissionLevel_value[strings.ToUpper(pod.Annotations[kubernetes.WorkspaceAdmissionAnnotation])]; ok {
admission = wsapi.AdmissionLevel(av)
}
imageSpec, _ := regapi.ImageSpecFromBase64(pod.Annotations[kubernetes.WorkspaceImageSpecAnnotation])
workspaceURL := pod.Annotations[kubernetes.WorkspaceURLAnnotation]
return &WorkspaceInfo{
WorkspaceID: pod.Labels[kubernetes.MetaIDLabel],
InstanceID: pod.Labels[kubernetes.WorkspaceIDLabel],
URL: workspaceURL,
IDEImage: imageSpec.IdeRef,
IDEPublicPort: getPortStr(workspaceURL),
SupervisorImage: imageSpec.SupervisorRef,
IPAddress: pod.Status.PodIP,
Ports: extractExposedPorts(pod).Ports,
Auth: &wsapi.WorkspaceAuthentication{Admission: admission, OwnerToken: ownerToken},
StartedAt: pod.CreationTimestamp.Time,
}
}
// WorkspaceInfo return the WorkspaceInfo available for the given workspaceID.
func (r *RemoteWorkspaceInfoProvider) WorkspaceInfo(workspaceID string) *WorkspaceInfo {
workspaces, err := r.store.ByIndex(workspaceIndex, workspaceID)
if err != nil {
return nil
}
if len(workspaces) == 1 {
return workspaces[0].(*WorkspaceInfo)
}
return nil
}
// getPortStr extracts the port part from a given URL string. Returns "" if parsing fails or port is not specified.
func getPortStr(urlStr string) string {
portURL, err := url.Parse(urlStr)
if err != nil {
log.WithField("url", urlStr).WithError(err).Error("error parsing URL while getting URL port")
return ""
}
if portURL.Port() == "" {
switch scheme := portURL.Scheme; scheme {
case "http":
return "80"
case "https":
return "443"
}
}
return portURL.Port()
}
type fixedInfoProvider struct {
Infos map[string]*WorkspaceInfo
}
// WorkspaceInfo returns the workspace information of a workspace using it's workspace ID.
func (fp *fixedInfoProvider) WorkspaceInfo(workspaceID string) *WorkspaceInfo {
if fp.Infos == nil {
return nil
}
return fp.Infos[workspaceID]
}
func extractExposedPorts(pod *corev1.Pod) *api.ExposedPorts {
if data, ok := pod.Annotations[kubernetes.WorkspaceExposedPorts]; ok {
ports, _ := api.ExposedPortsFromBase64(data)
return ports
}
return &api.ExposedPorts{}
}