Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion pkg/admin/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func (l *adminService) CreateBridge(ctx context.Context, req *bridgev1.CreateBri
bundle, err = resources.SourceFromManifests(req.SourceManifests)
} else if req.SourceDeployment != "" {
logger.Info("Creating bridge")
bundle, err = resources.SourceFromNamespace(ctx, l.client, targetNS, req.SourceDeployment)
bundle, err = resources.SourceFromNamespace(ctx, l.dynClient, targetNS, req.SourceDeployment)
} else {
logger.Info("Creating simple bridge")
bundle, err = resources.SourceSimple(targetNS, proxyImage)
Expand Down
3 changes: 2 additions & 1 deletion pkg/commands/administrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ func (s *administratorServer) DeleteBridge(ctx context.Context, req *bridgev1.De
// gRPC status error.
func grpcError(err error) error {
var notFound *resources.DeploymentNotFoundError
if errors.As(err, &notFound) {
var workloadNotFound *resources.WorkloadNotFoundError
if errors.As(err, &notFound) || errors.As(err, &workloadNotFound) {
return status.Error(codes.NotFound, err.Error())
}
return status.Error(codes.Internal, err.Error())
Expand Down
11 changes: 11 additions & 0 deletions pkg/k8s/resources/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ const (
DefaultWorkloadKind = "Deployment"
)

// WorkloadNotFoundError is returned when no workload of any supported kind has
// the source name.
type WorkloadNotFoundError struct {
Name string
Namespace string
}

func (e *WorkloadNotFoundError) Error() string {
return fmt.Sprintf("no workload found named '%s' in namespace '%s'", e.Name, e.Namespace)
}

// DeploymentNotFoundError is returned when the source deployment does not exist.
type DeploymentNotFoundError struct {
Name string
Expand Down
39 changes: 23 additions & 16 deletions pkg/k8s/resources/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,34 @@ import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/dynamic"
)

// SourceFromNamespace fetches a source deployment from the cluster and returns
// a Bundle containing just that deployment. Only the deployment spec is used —
// not the live pod — so that webhook-injected env vars and volume mounts (e.g.
// IRSA) are absent and get cleanly re-injected on the bridge pod.
func SourceFromNamespace(ctx context.Context, client kubernetes.Interface, namespace, deployment string) (*Bundle, error) {
srcDeploy, err := client.AppsV1().Deployments(namespace).Get(ctx, deployment, metav1.GetOptions{})
if err != nil {
// SourceFromNamespace fetches the named workload from the cluster, trying each
// of workloadKinds in order, and returns a Bundle holding it as a Deployment
// (see DeploymentFromWorkload). Only the workload's pod template is used — not a
// live pod — so that webhook-injected env vars and volume mounts (e.g. IRSA)
// are absent and get cleanly re-injected on the bridge pod.
func SourceFromNamespace(ctx context.Context, client dynamic.Interface, namespace, name string) (*Bundle, error) {
for _, kind := range workloadKinds {
obj, err := client.Resource(gvkToGVR(kind.GVK())).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
if errors.IsNotFound(err) {
return nil, &DeploymentNotFoundError{Name: deployment, Namespace: namespace}
continue
}
return nil, fmt.Errorf("failed to get source deployment %s/%s: %w", namespace, deployment, err)
if err != nil {
return nil, fmt.Errorf("failed to get source %s %s/%s: %w", kind.GVK().Kind, namespace, name, err)
}
deploy, err := DeploymentFromWorkload(kind, obj)
if err != nil {
return nil, err
}
return &Bundle{
Resources: []Resource{
{Object: deploy, GVK: appsv1.SchemeGroupVersion.WithKind("Deployment")},
},
}, nil
}

return &Bundle{
Resources: []Resource{
{Object: srcDeploy, GVK: appsv1.SchemeGroupVersion.WithKind("Deployment")},
},
}, nil
return nil, &WorkloadNotFoundError{Name: name, Namespace: namespace}
}

// SourceSimple builds a minimal Deployment with just the bridge proxy container.
Expand Down
27 changes: 24 additions & 3 deletions pkg/k8s/resources/source_manifests.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"log/slog"
"slices"
"strings"

"github.com/vercel/bridge/pkg/archive"
Expand All @@ -20,14 +21,20 @@ import (

// SourceFromManifests unpacks a tar.gz archive of Kubernetes YAML files, parses
// every document into typed or unstructured resources, and returns a Bundle
// ready for the transform pipeline.
// ready for the transform pipeline. Every workload, whatever its kind, becomes a
// Deployment (see DeploymentFromWorkload), ordered by workloadKinds.
func SourceFromManifests(manifests []byte) (*Bundle, error) {
fileMap, err := archive.Unpack(manifests)
if err != nil {
return nil, fmt.Errorf("failed to unpack source manifests: %w", err)
}

var resources []Resource
type workload struct {
rank int
deploy *appsv1.Deployment
}
var workloads []workload

decoder := scheme.Codecs.UniversalDeserializer()

Expand All @@ -38,6 +45,17 @@ func SourceFromManifests(manifests []byte) (*Bundle, error) {
continue
}

if u, err := decodeUnstructured(doc); err == nil {
if i := workloadKindIndex(u.GroupVersionKind()); i >= 0 {
deploy, err := DeploymentFromWorkload(workloadKinds[i], u)
if err != nil {
return nil, fmt.Errorf("%s: %w", name, err)
}
workloads = append(workloads, workload{rank: i, deploy: deploy})
continue
}
}

obj, gvk, err := decoder.Decode(doc, nil, nil)
if err != nil {
// Try as unstructured for unknown types.
Expand All @@ -51,8 +69,6 @@ func SourceFromManifests(manifests []byte) (*Bundle, error) {
}

switch obj.(type) {
case *appsv1.Deployment:
resources = append(resources, Resource{Object: obj, GVK: *gvk})
case *corev1.Service, *corev1.ConfigMap, *corev1.Secret, *corev1.ServiceAccount:
resources = append(resources, Resource{Object: obj, GVK: *gvk})
default:
Expand All @@ -65,6 +81,11 @@ func SourceFromManifests(manifests []byte) (*Bundle, error) {
}
}

slices.SortStableFunc(workloads, func(a, b workload) int { return a.rank - b.rank })
for _, w := range workloads {
resources = append(resources, Resource{Object: w.deploy, GVK: appsv1.SchemeGroupVersion.WithKind("Deployment")})
}

return &Bundle{
Resources: resources,
}, nil
Expand Down
128 changes: 128 additions & 0 deletions pkg/k8s/resources/workload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package resources

import (
"fmt"
"maps"
"strings"

appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)

// WorkloadKind is a kind of workload a bridge can be created from.
type WorkloadKind interface {
// GVK is the workload's group, version and kind.
GVK() schema.GroupVersionKind
// PodTemplatePath is the field path of the workload's pod template.
PodTemplatePath() []string
}

type deploymentKind struct{}

func (*deploymentKind) GVK() schema.GroupVersionKind {
return appsv1.SchemeGroupVersion.WithKind("Deployment")
}
func (*deploymentKind) PodTemplatePath() []string { return []string{"spec", "template"} }

type statefulSetKind struct{}

func (*statefulSetKind) GVK() schema.GroupVersionKind {
return appsv1.SchemeGroupVersion.WithKind("StatefulSet")
}
func (*statefulSetKind) PodTemplatePath() []string { return []string{"spec", "template"} }

type daemonSetKind struct{}

func (*daemonSetKind) GVK() schema.GroupVersionKind {
return appsv1.SchemeGroupVersion.WithKind("DaemonSet")
}
func (*daemonSetKind) PodTemplatePath() []string { return []string{"spec", "template"} }

type replicaSetKind struct{}

func (*replicaSetKind) GVK() schema.GroupVersionKind {
return appsv1.SchemeGroupVersion.WithKind("ReplicaSet")
}
func (*replicaSetKind) PodTemplatePath() []string { return []string{"spec", "template"} }

type jobKind struct{}

func (*jobKind) GVK() schema.GroupVersionKind {
return batchv1.SchemeGroupVersion.WithKind("Job")
}
func (*jobKind) PodTemplatePath() []string { return []string{"spec", "template"} }

type cronJobKind struct{}

func (*cronJobKind) GVK() schema.GroupVersionKind {
return batchv1.SchemeGroupVersion.WithKind("CronJob")
}
func (*cronJobKind) PodTemplatePath() []string {
return []string{"spec", "jobTemplate", "spec", "template"}
}

// workloadKinds are the workload kinds a bridge can be created from, in the
// order a name is looked up and a bundle's workloads are ordered.
var workloadKinds = []WorkloadKind{
&deploymentKind{},
&statefulSetKind{},
&daemonSetKind{},
&replicaSetKind{},
&jobKind{},
&cronJobKind{},
}

// workloadKindIndex returns gvk's position in workloadKinds, or -1 if it isn't a
// workload kind.
func workloadKindIndex(gvk schema.GroupVersionKind) int {
for i, kind := range workloadKinds {
if k := kind.GVK(); k.Group == gvk.Group && k.Kind == gvk.Kind {
return i
}
}
return -1
}

// DeploymentFromWorkload returns a single-replica Deployment built from a
// workload's pod template and metadata, so the rest of the pipeline only ever
// deals with Deployments.
//
// A bridge built from a Job or CronJob runs its pod, not its schedule or
// completion: run the job in the devcontainer with `bridge exec`.
func DeploymentFromWorkload(kind WorkloadKind, obj *unstructured.Unstructured) (*appsv1.Deployment, error) {
path := kind.PodTemplatePath()
raw, found, err := unstructured.NestedMap(obj.Object, path...)
if err != nil || !found {
return nil, fmt.Errorf("%s %s has no pod template at %s", kind.GVK().Kind, obj.GetName(), strings.Join(path, "."))
}
var template corev1.PodTemplateSpec
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw, &template); err != nil {
return nil, fmt.Errorf("invalid pod template in %s %s: %w", kind.GVK().Kind, obj.GetName(), err)
}
// A Deployment's pods must restart Always and can't set a deadline.
template.Spec.RestartPolicy = corev1.RestartPolicyAlways
template.Spec.ActiveDeadlineSeconds = nil
if len(template.Labels) == 0 {
template.Labels = map[string]string{"app": obj.GetName()}
}

replicas := int32(1)
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: obj.GetName(),
Namespace: obj.GetNamespace(),
Labels: obj.GetLabels(),
Annotations: obj.GetAnnotations(),
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{MatchLabels: maps.Clone(template.Labels)},
Template: template,
},
}, nil
}
Loading
Loading