gitpod/components/ws-proxy/pkg/proxy/infoprovider.go
Pudong 7f83e2f592
[ws-proxy] only get username if workspace not managed by mk2 (#19180)
* [ws-proxy] only get username if workspace not managed by mk2

* remove ssh key from infoprovider

* improve logs

* Update components/ws-proxy/pkg/sshproxy/server.go
2023-12-01 21:41:09 +02:00

205 lines
5.9 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"
"sort"
"golang.org/x/xerrors"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/cache"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
wsk8s "github.com/gitpod-io/gitpod/common-go/kubernetes"
"github.com/gitpod-io/gitpod/common-go/log"
wsapi "github.com/gitpod-io/gitpod/ws-manager/api"
workspacev1 "github.com/gitpod-io/gitpod/ws-manager/api/crd/v1"
"github.com/gitpod-io/gitpod/ws-proxy/pkg/common"
)
const (
workspaceIndex = "workspaceIndex"
)
// 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 CRDWorkspaceInfoProvider struct {
client.Client
Scheme *runtime.Scheme
store cache.ThreadSafeStore
}
// NewCRDWorkspaceInfoProvider creates a fresh WorkspaceInfoProvider.
func NewCRDWorkspaceInfoProvider(client client.Client, scheme *runtime.Scheme) (*CRDWorkspaceInfoProvider, error) {
// create custom indexer for searches
indexers := cache.Indexers{
workspaceIndex: func(obj interface{}) ([]string, error) {
if workspaceInfo, ok := obj.(*common.WorkspaceInfo); ok {
return []string{workspaceInfo.WorkspaceID}, nil
}
return nil, xerrors.Errorf("object is not a WorkspaceInfo")
},
}
return &CRDWorkspaceInfoProvider{
Client: client,
Scheme: scheme,
store: cache.NewThreadSafeStore(indexers, cache.Indices{}),
}, nil
}
// WorkspaceInfo return the WorkspaceInfo available for the given workspaceID.
func (r *CRDWorkspaceInfoProvider) WorkspaceInfo(workspaceID string) *common.WorkspaceInfo {
workspaces, err := r.store.ByIndex(workspaceIndex, workspaceID)
if err != nil {
return nil
}
if len(workspaces) >= 1 {
if len(workspaces) != 1 {
log.Warnf("multiple instances (%d) for workspace %s", len(workspaces), workspaceID)
}
sort.Slice(workspaces, func(i, j int) bool {
a := workspaces[i].(*common.WorkspaceInfo)
b := workspaces[j].(*common.WorkspaceInfo)
return a.StartedAt.After(b.StartedAt)
})
return workspaces[0].(*common.WorkspaceInfo)
}
return nil
}
func (r *CRDWorkspaceInfoProvider) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var ws workspacev1.Workspace
err := r.Client.Get(context.Background(), req.NamespacedName, &ws)
if errors.IsNotFound(err) {
// workspace is gone - that's ok
r.store.Delete(req.Name)
log.WithField("workspacepod", req.Name).Debug("removing workspace from store")
return reconcile.Result{}, nil
}
var podIP string
if ws.Status.Runtime != nil {
podIP = ws.Status.Runtime.PodIP
}
ports := make([]*wsapi.PortSpec, 0, len(ws.Spec.Ports))
for _, p := range ws.Spec.Ports {
v := wsapi.PortVisibility_PORT_VISIBILITY_PRIVATE
protocol := wsapi.PortProtocol_PORT_PROTOCOL_HTTP
if p.Visibility == workspacev1.AdmissionLevelEveryone {
v = wsapi.PortVisibility_PORT_VISIBILITY_PUBLIC
}
if p.Protocol == workspacev1.PortProtocolHttps {
protocol = wsapi.PortProtocol_PORT_PROTOCOL_HTTPS
}
ports = append(ports, &wsapi.PortSpec{
Port: p.Port,
Visibility: v,
Protocol: protocol,
})
}
admission := wsapi.AdmissionLevel_ADMIT_OWNER_ONLY
if ws.Spec.Admission.Level == workspacev1.AdmissionLevelEveryone {
admission = wsapi.AdmissionLevel_ADMIT_EVERYONE
}
managedByMk2 := true
if managedBy, ok := ws.Labels[wsk8s.WorkspaceManagedByLabel]; ok && managedBy != "ws-manager-mk2" {
managedByMk2 = false
}
wsinfo := &common.WorkspaceInfo{
WorkspaceID: ws.Spec.Ownership.WorkspaceID,
InstanceID: ws.Name,
URL: ws.Status.URL,
IDEImage: ws.Spec.Image.IDE.Web,
SupervisorImage: ws.Spec.Image.IDE.Supervisor,
IDEPublicPort: getPortStr(ws.Status.URL),
IPAddress: podIP,
Ports: ports,
Auth: &wsapi.WorkspaceAuthentication{Admission: admission, OwnerToken: ws.Status.OwnerToken},
StartedAt: ws.CreationTimestamp.Time,
OwnerUserId: ws.Spec.Ownership.Owner,
SSHPublicKeys: ws.Spec.SshPublicKeys,
IsRunning: ws.Status.Phase == workspacev1.WorkspacePhaseRunning,
IsEnabledSSHCA: ws.Spec.SSHGatewayCAPublicKey != "",
IsManagedByMk2: managedByMk2,
}
r.store.Update(req.Name, wsinfo)
log.WithField("workspace", req.Name).WithField("details", wsinfo).Debug("adding/updating workspace details")
return ctrl.Result{}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *CRDWorkspaceInfoProvider) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
Named("workspacecrd").
WithEventFilter(predicate.ResourceVersionChangedPredicate{}).
For(
&workspacev1.Workspace{},
).
Complete(r)
}
// CompositeInfoProvider checks each of its info providers and returns the first info found.
type CompositeInfoProvider []common.WorkspaceInfoProvider
func (c CompositeInfoProvider) WorkspaceInfo(workspaceID string) *common.WorkspaceInfo {
for _, ip := range c {
res := ip.WorkspaceInfo(workspaceID)
if res != nil {
return res
}
}
return nil
}
type fixedInfoProvider struct {
Infos map[string]*common.WorkspaceInfo
}
// WorkspaceInfo returns the workspace information of a workspace using it's workspace ID.
func (fp *fixedInfoProvider) WorkspaceInfo(workspaceID string) *common.WorkspaceInfo {
if fp.Infos == nil {
return nil
}
return fp.Infos[workspaceID]
}