fix(ws): add secrets field to backend API schema (#331)

related: #239

This commit brings partial support for secrets to the backend API.  It enables the `frontend` component to successfully create a Workspace through the "wizard flow".

**HOWEVER**, it is important to note this secrets attribute is not supported within the `controller` component yet - as #240 is not yet merged. To unblock the `frontend` - the logic contained in this commit simply adds the necessary scaffolding to accept the `secrets` attribute defined within `volumes`.  Once umarshalled, the backend essentially ignores this data.  Code to fully enable the end to end flow is included in this PR - but simply commented out with `TODO:` comments denoting what can be uncommented once #240 is merged into `notebooks-v2`.  A test is also presently disabled with `XIt` - and can also be enabled when required code present.

Changes were initially coded against the branch provided on #240 to verify full end-to-end behavior.  Once confirmed, commit was rebased onto `notebooks-v2`, relevant code commented out as described above, and behavior retested to ensure desired outcome.

In summary, with these changes:
- `backend` API accepts `volumes.secrets` in the _Create_ payload
- secrets data is **NOT USED** when programmatically constructing the Workspace CR
- Resultant workspace has no `secrets` data - irrespective of it if was provided in the payload or not.

Signed-off-by: Andy Stoneberg <astonebe@redhat.com>
This commit is contained in:
Andy Stoneberg 2025-05-15 13:03:23 -04:00 committed by GitHub
parent 45d778a972
commit ea93acc140
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 216 additions and 4 deletions

View File

@ -770,6 +770,9 @@ var _ = Describe("Workspaces Handler", func() {
}
Expect(createdWorkspace.Spec.PodTemplate.Volumes.Data).To(Equal(expected))
// TODO: Verify secrets once #240 merged
// Expect(createdWorkspace.Spec.PodTemplate.Volumes.Secrets).To(BeEmpty())
By("creating an HTTP request to delete the Workspace")
path = strings.Replace(WorkspacesByNamePath, ":"+NamespacePathParam, namespaceNameCrud, 1)
path = strings.Replace(path, ":"+ResourceNamePathParam, workspaceName, 1)
@ -805,6 +808,70 @@ var _ = Describe("Workspaces Handler", func() {
Expect(apierrors.IsNotFound(err)).To(BeTrue())
})
// TODO: Change to It when #240 merged
XIt("should create a workspace with secrets", func() {
// Create a workspace with secrets
workspace := &models.WorkspaceCreate{
Name: "test-workspace",
Kind: "test-kind",
PodTemplate: models.PodTemplateMutate{
Options: models.PodTemplateOptionsMutate{
ImageConfig: "test-image",
PodConfig: "test-config",
},
Volumes: models.PodVolumesMutate{
Data: []models.PodVolumeMount{
{
PVCName: "test-pvc",
MountPath: "/data",
},
},
Secrets: []models.PodSecretMount{
{
SecretName: "test-secret",
MountPath: "/secrets",
DefaultMode: int32(0o644),
},
},
},
},
}
// Create the workspace using the API handler
bodyEnvelope := WorkspaceCreateEnvelope{Data: workspace}
bodyEnvelopeJSON, err := json.Marshal(bodyEnvelope)
Expect(err).NotTo(HaveOccurred())
path := strings.Replace(WorkspacesByNamespacePath, ":"+NamespacePathParam, namespaceNameCrud, 1)
req, err := http.NewRequest(http.MethodPost, path, strings.NewReader(string(bodyEnvelopeJSON)))
Expect(err).NotTo(HaveOccurred())
req.Header.Set("Content-Type", "application/json")
req.Header.Set(userIdHeader, adminUser)
rr := httptest.NewRecorder()
ps := httprouter.Params{
httprouter.Param{
Key: NamespacePathParam,
Value: namespaceNameCrud,
},
}
a.CreateWorkspaceHandler(rr, req, ps)
rs := rr.Result()
defer rs.Body.Close()
Expect(rs.StatusCode).To(Equal(http.StatusCreated), descUnexpectedHTTPStatus, rr.Body.String())
// Get the created workspace
createdWorkspace := &kubefloworgv1beta1.Workspace{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: workspace.Name, Namespace: namespaceNameCrud}, createdWorkspace)).To(Succeed())
// TODO: Verify the secrets are properly set once #240 merged
// Expect(createdWorkspace.Spec.PodTemplate.Volumes.Secrets).To(HaveLen(1))
// Expect(createdWorkspace.Spec.PodTemplate.Volumes.Secrets[0].SecretName).To(Equal("test-secret"))
// Expect(createdWorkspace.Spec.PodTemplate.Volumes.Secrets[0].MountPath).To(Equal("/secrets"))
// Expect(createdWorkspace.Spec.PodTemplate.Volumes.Secrets[0].DefaultMode).To(Equal(int32(0o644)))
})
// TODO: test when fail to create a Workspace when:
// - body payload invalid (missing name/kind, and/or non RCF 1123 name)
// - invalid namespace HTTP path parameter (also test for other API handlers)

View File

@ -67,8 +67,9 @@ type PodMetadata struct {
}
type PodVolumes struct {
Home *PodVolumeInfo `json:"home,omitempty"`
Data []PodVolumeInfo `json:"data"`
Home *PodVolumeInfo `json:"home,omitempty"`
Data []PodVolumeInfo `json:"data"`
Secrets []PodSecretInfo `json:"secrets,omitempty"`
}
type PodVolumeInfo struct {
@ -77,6 +78,12 @@ type PodVolumeInfo struct {
ReadOnly bool `json:"readOnly"`
}
type PodSecretInfo struct {
SecretName string `json:"secretName"`
MountPath string `json:"mountPath"`
DefaultMode int32 `json:"defaultMode,omitempty"`
}
type PodTemplateOptions struct {
ImageConfig ImageConfig `json:"imageConfig"`
PodConfig PodConfig `json:"podConfig"`

View File

@ -43,8 +43,9 @@ type PodMetadataMutate struct {
}
type PodVolumesMutate struct {
Home *string `json:"home,omitempty"`
Data []PodVolumeMount `json:"data"`
Home *string `json:"home,omitempty"`
Data []PodVolumeMount `json:"data"`
Secrets []PodSecretMount `json:"secrets,omitempty"`
}
type PodVolumeMount struct {
@ -53,6 +54,12 @@ type PodVolumeMount struct {
ReadOnly bool `json:"readOnly,omitempty"`
}
type PodSecretMount struct {
SecretName string `json:"secretName"`
MountPath string `json:"mountPath"`
DefaultMode int32 `json:"defaultMode,omitempty"`
}
type PodTemplateOptionsMutate struct {
ImageConfig string `json:"imageConfig"`
PodConfig string `json:"podConfig"`
@ -87,5 +94,18 @@ func (w *WorkspaceCreate) Validate(prefix *field.Path) []*field.Error {
errs = append(errs, helper.ValidateFieldIsNotEmpty(volumePath.Child("mountPath"), volume.MountPath)...)
}
// validate the secrets
secretsPath := prefix.Child("podTemplate", "volumes", "secrets")
for i, secret := range w.PodTemplate.Volumes.Secrets {
secretPath := secretsPath.Index(i)
errs = append(errs, helper.ValidateFieldIsNotEmpty(secretPath.Child("secretName"), secret.SecretName)...)
errs = append(errs, helper.ValidateFieldIsNotEmpty(secretPath.Child("mountPath"), secret.MountPath)...)
if secret.DefaultMode != 0 {
if secret.DefaultMode < 0 || secret.DefaultMode > 511 {
errs = append(errs, field.Invalid(secretPath.Child("defaultMode"), secret.DefaultMode, "defaultMode must be between 0 and 511"))
}
}
}
return errs
}

View File

@ -137,6 +137,17 @@ func (r *WorkspaceRepository) CreateWorkspace(ctx context.Context, workspaceCrea
}
}
// get secrets from workspace model
// TODO: uncomment this once #240 is merged
// secretMounts := make([]kubefloworgv1beta1.PodSecretMount, len(workspaceCreate.PodTemplate.Volumes.Secrets))
// for i, secret := range workspaceCreate.PodTemplate.Volumes.Secrets {
// secretMounts[i] = kubefloworgv1beta1.PodSecretMount{
// SecretName: secret.SecretName,
// MountPath: secret.MountPath,
// DefaultMode: secret.DefaultMode,
// }
// }
// define workspace object from model
workspaceName := workspaceCreate.Name
workspaceKindName := workspaceCreate.Kind
@ -157,6 +168,7 @@ func (r *WorkspaceRepository) CreateWorkspace(ctx context.Context, workspaceCrea
Volumes: kubefloworgv1beta1.WorkspacePodVolumes{
Home: workspaceCreate.PodTemplate.Volumes.Home,
Data: dataVolumeMounts,
// Secrets: secretMounts,
},
Options: kubefloworgv1beta1.WorkspacePodOptions{
ImageConfig: workspaceCreate.PodTemplate.Options.ImageConfig,

View File

@ -1072,6 +1072,34 @@ const docTemplate = `{
}
}
},
"workspaces.PodSecretInfo": {
"type": "object",
"properties": {
"defaultMode": {
"type": "integer"
},
"mountPath": {
"type": "string"
},
"secretName": {
"type": "string"
}
}
},
"workspaces.PodSecretMount": {
"type": "object",
"properties": {
"defaultMode": {
"type": "integer"
},
"mountPath": {
"type": "string"
},
"secretName": {
"type": "string"
}
}
},
"workspaces.PodTemplate": {
"type": "object",
"properties": {
@ -1161,6 +1189,12 @@ const docTemplate = `{
},
"home": {
"$ref": "#/definitions/workspaces.PodVolumeInfo"
},
"secrets": {
"type": "array",
"items": {
"$ref": "#/definitions/workspaces.PodSecretInfo"
}
}
}
},
@ -1175,6 +1209,12 @@ const docTemplate = `{
},
"home": {
"type": "string"
},
"secrets": {
"type": "array",
"items": {
"$ref": "#/definitions/workspaces.PodSecretMount"
}
}
}
},

View File

@ -1070,6 +1070,34 @@
}
}
},
"workspaces.PodSecretInfo": {
"type": "object",
"properties": {
"defaultMode": {
"type": "integer"
},
"mountPath": {
"type": "string"
},
"secretName": {
"type": "string"
}
}
},
"workspaces.PodSecretMount": {
"type": "object",
"properties": {
"defaultMode": {
"type": "integer"
},
"mountPath": {
"type": "string"
},
"secretName": {
"type": "string"
}
}
},
"workspaces.PodTemplate": {
"type": "object",
"properties": {
@ -1159,6 +1187,12 @@
},
"home": {
"$ref": "#/definitions/workspaces.PodVolumeInfo"
},
"secrets": {
"type": "array",
"items": {
"$ref": "#/definitions/workspaces.PodSecretInfo"
}
}
}
},
@ -1173,6 +1207,12 @@
},
"home": {
"type": "string"
},
"secrets": {
"type": "array",
"items": {
"$ref": "#/definitions/workspaces.PodSecretMount"
}
}
}
},

View File

@ -356,6 +356,24 @@ definitions:
type: string
type: object
type: object
workspaces.PodSecretInfo:
properties:
defaultMode:
type: integer
mountPath:
type: string
secretName:
type: string
type: object
workspaces.PodSecretMount:
properties:
defaultMode:
type: integer
mountPath:
type: string
secretName:
type: string
type: object
workspaces.PodTemplate:
properties:
options:
@ -414,6 +432,10 @@ definitions:
type: array
home:
$ref: '#/definitions/workspaces.PodVolumeInfo'
secrets:
items:
$ref: '#/definitions/workspaces.PodSecretInfo'
type: array
type: object
workspaces.PodVolumesMutate:
properties:
@ -423,6 +445,10 @@ definitions:
type: array
home:
type: string
secrets:
items:
$ref: '#/definitions/workspaces.PodSecretMount'
type: array
type: object
workspaces.ProbeResult:
enum: