Skip to content

Commit 4f86816

Browse files
committed
feat: add workloadIdentity field to SpinApp
This adds a new 'workloadIdentity' field to the SpinApp CRD and the controller will detect this field to determine whether or not to apply provider specific labels to enable cloud workload identity. See SKIP spinframework/skips#16 for more details Signed-off-by: Jiaxiao (mossaka) Zhou <[email protected]>
1 parent 9c326eb commit 4f86816

File tree

5 files changed

+154
-4
lines changed

5 files changed

+154
-4
lines changed

api/v1alpha1/spinapp_types.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ type SpinAppSpec struct {
8686
// If this is not provided all components are executed.
8787
// +kubebuilder:validation:MinItems:=1
8888
Components []string `json:"components,omitempty"`
89+
90+
// WorkloadIdentity defines the workload identity configuration for cloud provider authentication.
91+
// +optional
92+
WorkloadIdentity *WorkloadIdentity `json:"workloadIdentity,omitempty"`
8993
}
9094

9195
// SpinAppStatus defines the observed state of SpinApp
@@ -287,6 +291,17 @@ type HTTPHealthProbeHeader struct {
287291
Value string `json:"value"`
288292
}
289293

294+
// WorkloadIdentity defines the configuration for cloud provider workload identity.
295+
type WorkloadIdentity struct {
296+
// ServiceAccountName is the name of the Kubernetes service account to use for workload identity.
297+
// +kubebuilder:validation:Required
298+
ServiceAccountName string `json:"serviceAccountName"`
299+
300+
// ProviderMetadata contains cloud provider-specific configuration for workload identity.
301+
// +kubebuilder:validation:Required
302+
ProviderMetadata map[string]string `json:"providerMetadata"`
303+
}
304+
290305
func init() {
291306
SchemeBuilder.Register(&SpinApp{}, &SpinAppList{})
292307
}

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/controller/spinapp_controller.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,15 @@ func constructDeployment(ctx context.Context, app *spinv1alpha1.SpinApp, config
422422
}
423423
maps.Copy(templateLabels, readyLabels)
424424

425+
// Add Azure workload identity label if configured
426+
if app.Spec.WorkloadIdentity != nil {
427+
if val, ok := app.Spec.WorkloadIdentity.ProviderMetadata["azure"]; ok {
428+
if val == "true" {
429+
templateLabels["azure.workload.identity/use"] = "true"
430+
}
431+
}
432+
}
433+
425434
// TODO: Once we land admission webhooks write some validation for this e.g.
426435
// don't allow setting memory limit with cyclotron runtime.
427436
resources := corev1.ResourceRequirements{
@@ -508,10 +517,11 @@ func constructDeployment(ctx context.Context, app *spinv1alpha1.SpinApp, config
508517
Annotations: templateAnnotations,
509518
},
510519
Spec: corev1.PodSpec{
511-
RuntimeClassName: config.RuntimeClassName,
512-
Containers: []corev1.Container{container},
513-
ImagePullSecrets: app.Spec.ImagePullSecrets,
514-
Volumes: volumes,
520+
RuntimeClassName: config.RuntimeClassName,
521+
ServiceAccountName: getServiceAccountName(app),
522+
Containers: []corev1.Container{container},
523+
ImagePullSecrets: app.Spec.ImagePullSecrets,
524+
Volumes: volumes,
515525
},
516526
},
517527
},
@@ -530,6 +540,16 @@ func constructDeployment(ctx context.Context, app *spinv1alpha1.SpinApp, config
530540
return dep, nil
531541
}
532542

543+
// getServiceAccountName returns the service account name to use for the deployment.
544+
// If workload identity is configured, it returns the configured service account name.
545+
// Otherwise, it returns "default" which is the Kubernetes default.
546+
func getServiceAccountName(app *spinv1alpha1.SpinApp) string {
547+
if app.Spec.WorkloadIdentity != nil {
548+
return app.Spec.WorkloadIdentity.ServiceAccountName
549+
}
550+
return "default"
551+
}
552+
533553
// findDeploymentForApp finds the deployment for a SpinApp.
534554
func (r *SpinAppReconciler) findDeploymentForApp(ctx context.Context, app *spinv1alpha1.SpinApp) (*appsv1.Deployment, error) {
535555
var deployment appsv1.Deployment

internal/controller/spinapp_controller_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,3 +630,74 @@ func TestReconcile_Integration_Deployment_SpinCAInjection(t *testing.T) {
630630
cancelFunc()
631631
wg.Wait()
632632
}
633+
634+
func TestReconcile_Integration_WorkloadIdentity_Azure(t *testing.T) {
635+
t.Parallel()
636+
637+
envTest, mgr, _ := setupController(t)
638+
639+
ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second)
640+
defer cancelFunc()
641+
642+
var wg sync.WaitGroup
643+
wg.Add(1)
644+
go func() {
645+
require.NoError(t, mgr.Start(ctx))
646+
wg.Done()
647+
}()
648+
649+
// Create an executor that creates a deployment
650+
executor := &spinv1alpha1.SpinAppExecutor{
651+
ObjectMeta: metav1.ObjectMeta{
652+
Name: "executor",
653+
Namespace: "default",
654+
},
655+
Spec: spinv1alpha1.SpinAppExecutorSpec{
656+
CreateDeployment: true,
657+
DeploymentConfig: &spinv1alpha1.ExecutorDeploymentConfig{
658+
RuntimeClassName: generics.Ptr("a-runtime-class"),
659+
},
660+
},
661+
}
662+
663+
require.NoError(t, envTest.k8sClient.Create(ctx, executor))
664+
665+
spinApp := &spinv1alpha1.SpinApp{
666+
ObjectMeta: metav1.ObjectMeta{
667+
Name: "app",
668+
Namespace: "default",
669+
},
670+
Spec: spinv1alpha1.SpinAppSpec{
671+
Executor: "executor",
672+
Image: "ghcr.io/radu-matei/perftest:v1",
673+
WorkloadIdentity: &spinv1alpha1.WorkloadIdentity{
674+
ServiceAccountName: "custom-sa",
675+
ProviderMetadata: map[string]string{
676+
"azure": "true",
677+
},
678+
},
679+
},
680+
}
681+
682+
require.NoError(t, envTest.k8sClient.Create(ctx, spinApp))
683+
684+
// Wait for the underlying deployment to exist
685+
var deployment appsv1.Deployment
686+
require.Eventually(t, func() bool {
687+
err := envTest.k8sClient.Get(ctx,
688+
types.NamespacedName{
689+
Namespace: "default",
690+
Name: "app"},
691+
&deployment)
692+
return err == nil
693+
}, 3*time.Second, 100*time.Millisecond)
694+
695+
// Verify Azure workload identity label is set
696+
require.Equal(t, "true", deployment.Spec.Template.ObjectMeta.Labels["azure.workload.identity/use"])
697+
// Verify service account name is set
698+
require.Equal(t, "custom-sa", deployment.Spec.Template.Spec.ServiceAccountName)
699+
700+
// Terminate the context to force the manager to shut down.
701+
cancelFunc()
702+
wg.Wait()
703+
}

internal/webhook/spinapp_validating.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ func (v *SpinAppValidator) validateSpinApp(ctx context.Context, spinApp *spinv1a
6464
if err := validateAnnotations(spinApp.Spec, executor); err != nil {
6565
allErrs = append(allErrs, err)
6666
}
67+
if err := validateWorkloadIdentity(spinApp.Spec); err != nil {
68+
allErrs = append(allErrs, err)
69+
}
6770
if len(allErrs) == 0 {
6871
return nil
6972
}
@@ -137,3 +140,17 @@ func validateAnnotations(spec spinv1alpha1.SpinAppSpec, executor *spinv1alpha1.S
137140

138141
return nil
139142
}
143+
144+
func validateWorkloadIdentity(spec spinv1alpha1.SpinAppSpec) *field.Error {
145+
if spec.WorkloadIdentity == nil {
146+
return nil
147+
}
148+
149+
if spec.WorkloadIdentity.ServiceAccountName == "" {
150+
return field.Required(
151+
field.NewPath("spec").Child("workloadIdentity").Child("serviceAccountName"),
152+
"serviceAccountName must be provided when workload identity is configured")
153+
}
154+
155+
return nil
156+
}

0 commit comments

Comments
 (0)