@@ -18,11 +18,15 @@ package api
1818
1919import  (
2020	"errors" 
21+ 	"fmt" 
22+ 	"io" 
2123	"net/http" 
2224
2325	"github.com/julienschmidt/httprouter" 
2426	kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1" 
27+ 	apierrors "k8s.io/apimachinery/pkg/api/errors" 
2528	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 
29+ 	"k8s.io/apimachinery/pkg/runtime" 
2630	"k8s.io/apimachinery/pkg/util/validation/field" 
2731
2832	"github.com/kubeflow/notebooks/workspaces/backend/internal/auth" 
@@ -31,6 +35,9 @@ import (
3135	repository "github.com/kubeflow/notebooks/workspaces/backend/internal/repositories/workspacekinds" 
3236)
3337
38+ // TODO: this should wrap the models.WorkspaceKindUpdate once we implement the update handler 
39+ type  WorkspaceKindCreateEnvelope  Envelope [* models.WorkspaceKind ]
40+ 
3441type  WorkspaceKindListEnvelope  Envelope [[]models.WorkspaceKind ]
3542
3643type  WorkspaceKindEnvelope  Envelope [models.WorkspaceKind ]
@@ -123,3 +130,95 @@ func (a *App) GetWorkspaceKindsHandler(w http.ResponseWriter, r *http.Request, _
123130	responseEnvelope  :=  & WorkspaceKindListEnvelope {Data : workspaceKinds }
124131	a .dataResponse (w , r , responseEnvelope )
125132}
133+ 
134+ // CreateWorkspaceKindHandler creates a new workspace kind. 
135+ // 
136+ //	@Summary		Create workspace kind 
137+ //	@Description	Creates a new workspace kind. 
138+ //	@Tags			workspacekinds 
139+ //	@Accept			application/yaml 
140+ //	@Produce		json 
141+ //	@Param			body	body		string					true	"Kubernetes YAML manifest of a WorkspaceKind" 
142+ //	@Success		201		{object}	WorkspaceKindEnvelope	"WorkspaceKind created successfully" 
143+ //	@Failure		400		{object}	ErrorEnvelope			"Bad Request." 
144+ //	@Failure		401		{object}	ErrorEnvelope			"Unauthorized. Authentication is required." 
145+ //	@Failure		403		{object}	ErrorEnvelope			"Forbidden. User does not have permission to create WorkspaceKind." 
146+ //	@Failure		409		{object}	ErrorEnvelope			"Conflict. WorkspaceKind with the same name already exists." 
147+ //	@Failure		413		{object}	ErrorEnvelope			"Request Entity Too Large. The request body is too large."" 
148+ //	@Failure		415		{object}	ErrorEnvelope			"Unsupported Media Type. Content-Type header is not correct." 
149+ //	@Failure		422		{object}	ErrorEnvelope			"Unprocessable Entity. Validation error." 
150+ //	@Failure		500		{object}	ErrorEnvelope			"Internal server error. An unexpected error occurred on the server." 
151+ //	@Router			/workspacekinds [post] 
152+ func  (a  * App ) CreateWorkspaceKindHandler (w  http.ResponseWriter , r  * http.Request , _  httprouter.Params ) {
153+ 
154+ 	// validate the Content-Type header 
155+ 	if  success  :=  a .ValidateContentType (w , r , MediaTypeYaml ); ! success  {
156+ 		return 
157+ 	}
158+ 
159+ 	// decode the request body 
160+ 	bodyBytes , err  :=  io .ReadAll (r .Body )
161+ 	if  err  !=  nil  {
162+ 		if  a .IsMaxBytesError (err ) {
163+ 			a .requestEntityTooLargeResponse (w , r , err )
164+ 			return 
165+ 		}
166+ 		a .badRequestResponse (w , r , err )
167+ 		return 
168+ 	}
169+ 	workspaceKind  :=  & kubefloworgv1beta1.WorkspaceKind {}
170+ 	err  =  runtime .DecodeInto (a .StrictYamlSerializer , bodyBytes , workspaceKind )
171+ 	if  err  !=  nil  {
172+ 		a .badRequestResponse (w , r , fmt .Errorf ("error decoding request body: %w" , err ))
173+ 		return 
174+ 	}
175+ 
176+ 	// validate the workspace kind 
177+ 	// NOTE: we only do basic validation so we know it's safe to send to the Kubernetes API server 
178+ 	//       comprehensive validation will be done by Kubernetes 
179+ 	// NOTE: checking the name field is non-empty also verifies that the workspace kind is not nil/empty 
180+ 	var  valErrs  field.ErrorList 
181+ 	wskNamePath  :=  field .NewPath ("metadata" , "name" )
182+ 	valErrs  =  append (valErrs , helper .ValidateFieldIsDNS1123Subdomain (wskNamePath , workspaceKind .Name )... )
183+ 	if  len (valErrs ) >  0  {
184+ 		a .failedValidationResponse (w , r , errMsgRequestBodyInvalid , valErrs , nil )
185+ 		return 
186+ 	}
187+ 
188+ 	// =========================== AUTH =========================== 
189+ 	authPolicies  :=  []* auth.ResourcePolicy {
190+ 		auth .NewResourcePolicy (
191+ 			auth .ResourceVerbCreate ,
192+ 			& kubefloworgv1beta1.WorkspaceKind {
193+ 				ObjectMeta : metav1.ObjectMeta {
194+ 					Name : workspaceKind .Name ,
195+ 				},
196+ 			},
197+ 		),
198+ 	}
199+ 	if  success  :=  a .requireAuth (w , r , authPolicies ); ! success  {
200+ 		return 
201+ 	}
202+ 	// ============================================================ 
203+ 
204+ 	createdWorkspaceKind , err  :=  a .repositories .WorkspaceKind .Create (r .Context (), workspaceKind )
205+ 	if  err  !=  nil  {
206+ 		if  errors .Is (err , repository .ErrWorkspaceKindAlreadyExists ) {
207+ 			a .conflictResponse (w , r , err )
208+ 			return 
209+ 		}
210+ 		if  apierrors .IsInvalid (err ) {
211+ 			causes  :=  helper .StatusCausesFromAPIStatus (err )
212+ 			a .failedValidationResponse (w , r , errMsgKubernetesValidation , nil , causes )
213+ 			return 
214+ 		}
215+ 		a .serverErrorResponse (w , r , fmt .Errorf ("error creating workspace kind: %w" , err ))
216+ 		return 
217+ 	}
218+ 
219+ 	// calculate the GET location for the created workspace kind (for the Location header) 
220+ 	location  :=  a .LocationGetWorkspaceKind (createdWorkspaceKind .Name )
221+ 
222+ 	responseEnvelope  :=  & WorkspaceKindCreateEnvelope {Data : createdWorkspaceKind }
223+ 	a .createdResponse (w , r , responseEnvelope , location )
224+ }
0 commit comments