From 409d5b9dc70682dec3c4667930af3c7b423dec11 Mon Sep 17 00:00:00 2001 From: Simon Murray Date: Wed, 2 Oct 2024 10:19:24 +0100 Subject: [PATCH] Die Applications This is being moved into its own separate opt-in application microservice, at long last. This also adds in a client, as the application service is going to have to have some smarts that clean up when clusters vanish at the very least. --- README.md | 2 +- charts/kubernetes/Chart.yaml | 4 +- pkg/client/client.go | 108 ++++++++++++++ pkg/openapi/client.go | 133 ----------------- pkg/openapi/router.go | 39 ----- pkg/openapi/schema.go | 173 ++++++++++------------- pkg/openapi/server.spec.yaml | 128 ----------------- pkg/openapi/types.go | 66 --------- pkg/server/handler/application/client.go | 117 --------------- pkg/server/handler/handler.go | 12 -- 10 files changed, 184 insertions(+), 598 deletions(-) create mode 100644 pkg/client/client.go delete mode 100644 pkg/server/handler/application/client.go diff --git a/README.md b/README.md index 51a34197..fcc4a238 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ metadata: spec: project: default source: - repoURL: https://unikorn-cloud.github.io/unikorn + repoURL: https://unikorn-cloud.github.io/kubernetes chart: unikorn targetRevision: v0.1.8 destination: diff --git a/charts/kubernetes/Chart.yaml b/charts/kubernetes/Chart.yaml index 5de0bc16..d2050c65 100644 --- a/charts/kubernetes/Chart.yaml +++ b/charts/kubernetes/Chart.yaml @@ -4,8 +4,8 @@ description: A Helm chart for deploying Unikorn Kubernetes Service type: application -version: v0.2.39 -appVersion: v0.2.39 +version: v0.2.40 +appVersion: v0.2.40 icon: https://raw.githubusercontent.com/unikorn-cloud/assets/main/images/logos/dark-on-light/icon.png diff --git a/pkg/client/client.go b/pkg/client/client.go new file mode 100644 index 00000000..d44537fc --- /dev/null +++ b/pkg/client/client.go @@ -0,0 +1,108 @@ +/* +Copyright 2024 the Unikorn Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package client + +import ( + "context" + "net/http" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + + coreclient "github.com/unikorn-cloud/core/pkg/client" + "github.com/unikorn-cloud/identity/pkg/middleware/authorization" + "github.com/unikorn-cloud/identity/pkg/middleware/openapi/accesstoken" + "github.com/unikorn-cloud/kubernetes/pkg/openapi" + + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Options = coreclient.HTTPOptions + +// NewOptions must be used to create options for consistency. +func NewOptions() *Options { + return coreclient.NewHTTPOptions("kubernetes") +} + +// Client wraps up the raw OpenAPI client with things to make it useable e.g. +// authorization and TLS. +type Client struct { + // client is a Kubenetes client. + client client.Client + // options allows setting of option from the CLI + options *Options + // clientOptions may be specified to inject client certificates etc. + clientOptions *coreclient.HTTPClientOptions +} + +// New creates a new client. +func New(client client.Client, options *Options, clientOptions *coreclient.HTTPClientOptions) *Client { + return &Client{ + client: client, + options: options, + clientOptions: clientOptions, + } +} + +// HTTPClient returns a new http client that will transparently do oauth2 header +// injection and refresh token updates. +func (c *Client) HTTPClient(ctx context.Context) (*http.Client, error) { + // Handle non-system CA certificates for the OIDC discovery protocol + // and oauth2 token refresh. This will return nil if none is specified + // and default to the system roots. + tlsClientConfig, err := coreclient.TLSClientConfig(ctx, c.client, c.options, c.clientOptions) + if err != nil { + return nil, err + } + + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsClientConfig, + }, + } + + return client, nil +} + +// accessTokenInjector implements OAuth2 bearer token authorization. +func accessTokenInjector(ctx context.Context, req *http.Request) error { + accessToken, err := accesstoken.FromContext(ctx) + if err != nil { + return err + } + + req.Header.Set("Authorization", "bearer "+accessToken) + otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header)) + authorization.InjectClientCert(ctx, req.Header) + + return nil +} + +// Client returns a new OpenAPI client that can be used to access the API. +func (c *Client) Client(ctx context.Context) (*openapi.ClientWithResponses, error) { + httpClient, err := c.HTTPClient(ctx) + if err != nil { + return nil, err + } + + client, err := openapi.NewClientWithResponses(c.options.Host(), openapi.WithHTTPClient(httpClient), openapi.WithRequestEditorFn(accessTokenInjector)) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/pkg/openapi/client.go b/pkg/openapi/client.go index d3b1555c..bea0c3e2 100644 --- a/pkg/openapi/client.go +++ b/pkg/openapi/client.go @@ -90,9 +90,6 @@ func WithRequestEditorFn(fn RequestEditorFn) ClientOption { // The interface specification for the client above. type ClientInterface interface { - // GetApiV1OrganizationsOrganizationIDApplications request - GetApiV1OrganizationsOrganizationIDApplications(ctx context.Context, organizationID OrganizationIDParameter, reqEditors ...RequestEditorFn) (*http.Response, error) - // GetApiV1OrganizationsOrganizationIDClustermanagers request GetApiV1OrganizationsOrganizationIDClustermanagers(ctx context.Context, organizationID OrganizationIDParameter, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -129,18 +126,6 @@ type ClientInterface interface { GetApiV1OrganizationsOrganizationIDProjectsProjectIDClustersClusterIDKubeconfig(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, clusterID ClusterIDParameter, reqEditors ...RequestEditorFn) (*http.Response, error) } -func (c *Client) GetApiV1OrganizationsOrganizationIDApplications(ctx context.Context, organizationID OrganizationIDParameter, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetApiV1OrganizationsOrganizationIDApplicationsRequest(c.Server, organizationID) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - func (c *Client) GetApiV1OrganizationsOrganizationIDClustermanagers(ctx context.Context, organizationID OrganizationIDParameter, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewGetApiV1OrganizationsOrganizationIDClustermanagersRequest(c.Server, organizationID) if err != nil { @@ -297,40 +282,6 @@ func (c *Client) GetApiV1OrganizationsOrganizationIDProjectsProjectIDClustersClu return c.Client.Do(req) } -// NewGetApiV1OrganizationsOrganizationIDApplicationsRequest generates requests for GetApiV1OrganizationsOrganizationIDApplications -func NewGetApiV1OrganizationsOrganizationIDApplicationsRequest(server string, organizationID OrganizationIDParameter) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "organizationID", runtime.ParamLocationPath, organizationID) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/api/v1/organizations/%s/applications", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("GET", queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - // NewGetApiV1OrganizationsOrganizationIDClustermanagersRequest generates requests for GetApiV1OrganizationsOrganizationIDClustermanagers func NewGetApiV1OrganizationsOrganizationIDClustermanagersRequest(server string, organizationID OrganizationIDParameter) (*http.Request, error) { var err error @@ -816,9 +767,6 @@ func WithBaseURL(baseURL string) ClientOption { // ClientWithResponsesInterface is the interface specification for the client with responses above. type ClientWithResponsesInterface interface { - // GetApiV1OrganizationsOrganizationIDApplicationsWithResponse request - GetApiV1OrganizationsOrganizationIDApplicationsWithResponse(ctx context.Context, organizationID OrganizationIDParameter, reqEditors ...RequestEditorFn) (*GetApiV1OrganizationsOrganizationIDApplicationsResponse, error) - // GetApiV1OrganizationsOrganizationIDClustermanagersWithResponse request GetApiV1OrganizationsOrganizationIDClustermanagersWithResponse(ctx context.Context, organizationID OrganizationIDParameter, reqEditors ...RequestEditorFn) (*GetApiV1OrganizationsOrganizationIDClustermanagersResponse, error) @@ -855,31 +803,6 @@ type ClientWithResponsesInterface interface { GetApiV1OrganizationsOrganizationIDProjectsProjectIDClustersClusterIDKubeconfigWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, clusterID ClusterIDParameter, reqEditors ...RequestEditorFn) (*GetApiV1OrganizationsOrganizationIDProjectsProjectIDClustersClusterIDKubeconfigResponse, error) } -type GetApiV1OrganizationsOrganizationIDApplicationsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *ApplicationResponse - JSON400 *externalRef0.BadRequestResponse - JSON401 *externalRef0.UnauthorizedResponse - JSON500 *externalRef0.InternalServerErrorResponse -} - -// Status returns HTTPResponse.Status -func (r GetApiV1OrganizationsOrganizationIDApplicationsResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r GetApiV1OrganizationsOrganizationIDApplicationsResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - type GetApiV1OrganizationsOrganizationIDClustermanagersResponse struct { Body []byte HTTPResponse *http.Response @@ -1118,15 +1041,6 @@ func (r GetApiV1OrganizationsOrganizationIDProjectsProjectIDClustersClusterIDKub return 0 } -// GetApiV1OrganizationsOrganizationIDApplicationsWithResponse request returning *GetApiV1OrganizationsOrganizationIDApplicationsResponse -func (c *ClientWithResponses) GetApiV1OrganizationsOrganizationIDApplicationsWithResponse(ctx context.Context, organizationID OrganizationIDParameter, reqEditors ...RequestEditorFn) (*GetApiV1OrganizationsOrganizationIDApplicationsResponse, error) { - rsp, err := c.GetApiV1OrganizationsOrganizationIDApplications(ctx, organizationID, reqEditors...) - if err != nil { - return nil, err - } - return ParseGetApiV1OrganizationsOrganizationIDApplicationsResponse(rsp) -} - // GetApiV1OrganizationsOrganizationIDClustermanagersWithResponse request returning *GetApiV1OrganizationsOrganizationIDClustermanagersResponse func (c *ClientWithResponses) GetApiV1OrganizationsOrganizationIDClustermanagersWithResponse(ctx context.Context, organizationID OrganizationIDParameter, reqEditors ...RequestEditorFn) (*GetApiV1OrganizationsOrganizationIDClustermanagersResponse, error) { rsp, err := c.GetApiV1OrganizationsOrganizationIDClustermanagers(ctx, organizationID, reqEditors...) @@ -1240,53 +1154,6 @@ func (c *ClientWithResponses) GetApiV1OrganizationsOrganizationIDProjectsProject return ParseGetApiV1OrganizationsOrganizationIDProjectsProjectIDClustersClusterIDKubeconfigResponse(rsp) } -// ParseGetApiV1OrganizationsOrganizationIDApplicationsResponse parses an HTTP response from a GetApiV1OrganizationsOrganizationIDApplicationsWithResponse call -func ParseGetApiV1OrganizationsOrganizationIDApplicationsResponse(rsp *http.Response) (*GetApiV1OrganizationsOrganizationIDApplicationsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &GetApiV1OrganizationsOrganizationIDApplicationsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest ApplicationResponse - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: - var dest externalRef0.BadRequestResponse - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON400 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: - var dest externalRef0.UnauthorizedResponse - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON401 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: - var dest externalRef0.InternalServerErrorResponse - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON500 = &dest - - } - - return response, nil -} - // ParseGetApiV1OrganizationsOrganizationIDClustermanagersResponse parses an HTTP response from a GetApiV1OrganizationsOrganizationIDClustermanagersWithResponse call func ParseGetApiV1OrganizationsOrganizationIDClustermanagersResponse(rsp *http.Response) (*GetApiV1OrganizationsOrganizationIDClustermanagersResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/pkg/openapi/router.go b/pkg/openapi/router.go index 3b9dd6d5..0b298401 100644 --- a/pkg/openapi/router.go +++ b/pkg/openapi/router.go @@ -15,9 +15,6 @@ import ( // ServerInterface represents all server handlers. type ServerInterface interface { - // (GET /api/v1/organizations/{organizationID}/applications) - GetApiV1OrganizationsOrganizationIDApplications(w http.ResponseWriter, r *http.Request, organizationID OrganizationIDParameter) - // (GET /api/v1/organizations/{organizationID}/clustermanagers) GetApiV1OrganizationsOrganizationIDClustermanagers(w http.ResponseWriter, r *http.Request, organizationID OrganizationIDParameter) @@ -50,11 +47,6 @@ type ServerInterface interface { type Unimplemented struct{} -// (GET /api/v1/organizations/{organizationID}/applications) -func (_ Unimplemented) GetApiV1OrganizationsOrganizationIDApplications(w http.ResponseWriter, r *http.Request, organizationID OrganizationIDParameter) { - w.WriteHeader(http.StatusNotImplemented) -} - // (GET /api/v1/organizations/{organizationID}/clustermanagers) func (_ Unimplemented) GetApiV1OrganizationsOrganizationIDClustermanagers(w http.ResponseWriter, r *http.Request, organizationID OrganizationIDParameter) { w.WriteHeader(http.StatusNotImplemented) @@ -109,34 +101,6 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(http.Handler) http.Handler -// GetApiV1OrganizationsOrganizationIDApplications operation middleware -func (siw *ServerInterfaceWrapper) GetApiV1OrganizationsOrganizationIDApplications(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - var err error - - // ------------- Path parameter "organizationID" ------------- - var organizationID OrganizationIDParameter - - err = runtime.BindStyledParameterWithLocation("simple", false, "organizationID", runtime.ParamLocationPath, chi.URLParam(r, "organizationID"), &organizationID) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "organizationID", Err: err}) - return - } - - ctx = context.WithValue(ctx, Oauth2AuthenticationScopes, []string{}) - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetApiV1OrganizationsOrganizationIDApplications(w, r, organizationID) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r.WithContext(ctx)) -} - // GetApiV1OrganizationsOrganizationIDClustermanagers operation middleware func (siw *ServerInterfaceWrapper) GetApiV1OrganizationsOrganizationIDClustermanagers(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -610,9 +574,6 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl ErrorHandlerFunc: options.ErrorHandlerFunc, } - r.Group(func(r chi.Router) { - r.Get(options.BaseURL+"/api/v1/organizations/{organizationID}/applications", wrapper.GetApiV1OrganizationsOrganizationIDApplications) - }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/api/v1/organizations/{organizationID}/clustermanagers", wrapper.GetApiV1OrganizationsOrganizationIDClustermanagers) }) diff --git a/pkg/openapi/schema.go b/pkg/openapi/schema.go index cab34d4c..e3a6b501 100644 --- a/pkg/openapi/schema.go +++ b/pkg/openapi/schema.go @@ -19,106 +19,79 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "+lB+EueNl/Pg7vzu/wMAAP//5tQKYCNeAAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/openapi/server.spec.yaml b/pkg/openapi/server.spec.yaml index c15a1184..214d53c0 100644 --- a/pkg/openapi/server.spec.yaml +++ b/pkg/openapi/server.spec.yaml @@ -256,25 +256,6 @@ paths: $ref: 'https://raw.githubusercontent.com/unikorn-cloud/core/main/pkg/openapi/common.spec.yaml#/components/responses/notFoundResponse' '500': $ref: 'https://raw.githubusercontent.com/unikorn-cloud/core/main/pkg/openapi/common.spec.yaml#/components/responses/internalServerErrorResponse' - /api/v1/organizations/{organizationID}/applications: - x-documentation-group: main - description: Cluster application services. - parameters: - - $ref: '#/components/parameters/organizationIDParameter' - get: - description: |- - Lists applications available to be installed on clusters. - security: - - oauth2Authentication: [] - responses: - '200': - $ref: '#/components/responses/applicationResponse' - '400': - $ref: 'https://raw.githubusercontent.com/unikorn-cloud/core/main/pkg/openapi/common.spec.yaml#/components/responses/badRequestResponse' - '401': - $ref: 'https://raw.githubusercontent.com/unikorn-cloud/core/main/pkg/openapi/common.spec.yaml#/components/responses/unauthorizedResponse' - '500': - $ref: 'https://raw.githubusercontent.com/unikorn-cloud/core/main/pkg/openapi/common.spec.yaml#/components/responses/internalServerErrorResponse' components: parameters: organizationIDParameter: @@ -489,92 +470,6 @@ components: type: array items: $ref: '#/components/schemas/kubernetesClusterRead' - applicationVersion: - description: An application version. - type: object - required: - - version - properties: - version: - description: The application's Helm chart version. - type: string - dependencies: - $ref: '#/components/schemas/applicationDependencies' - recommends: - $ref: '#/components/schemas/applicationRecommends' - applicationVersions: - description: A set of application versions. - type: array - items: - $ref: '#/components/schemas/applicationVersion' - applicationTags: - description: A set of tags for filtering applications. - type: array - items: - description: An application tag. - type: string - applicationDependency: - description: An application dependency. - type: object - required: - - name - properties: - name: - description: The application name. - type: string - applicationDependencies: - description: A set of applications that will be installed before this application. - type: array - items: - $ref: '#/components/schemas/applicationDependency' - applicationRecommends: - description: A set of recommended application that may be installed after this application. - type: array - items: - $ref: '#/components/schemas/applicationDependency' - applicationSpec: - description: An application. - type: object - required: - - humanReadableName - - documentation - - license - - icon - - versions - properties: - humanReadableName: - description: Human readable application name. - type: string - documentation: - description: Documentation link for the application. - type: string - format: uri - license: - description: The license under which the application is released. - type: string - icon: - description: A base64 encoded SVG icon. This should work in both light and dark themes. - type: string - format: byte - versions: - $ref: '#/components/schemas/applicationVersions' - tags: - $ref: '#/components/schemas/applicationTags' - applicationRead: - type: object - required: - - spec - - metadata - properties: - metadata: - $ref: 'https://raw.githubusercontent.com/unikorn-cloud/core/main/pkg/openapi/common.spec.yaml#/components/schemas/resourceReadMetadata' - spec: - $ref: '#/components/schemas/applicationSpec' - applications: - description: A list of appications. - type: array - items: - $ref: '#/components/schemas/applicationRead' requestBodies: createControlPlaneRequest: description: Control plane request parameters. @@ -693,29 +588,6 @@ components: size: 50 flavorId: c7568e2d-f9ab-453d-9a3a-51375f78426b replicas: 3 - applicationResponse: - description: A list of available applications. - content: - application/json: - schema: - $ref: '#/components/schemas/applications' - example: - - metadata: - id: c7568e2d-f9ab-453d-9a3a-51375f78426b - name: longhorn - description: |- - Provides an alternative replicated storage provider that distributes storage across - the Kubernetes cluster. Features high-availabilty, incremental snapshots and backups, - disaster recovery, and shared file mounts (read/write many). - creationTime: 2023-07-31T10:45:45Z - provisioningStatus: unknown - spec: - documentation: https://longhorn.io/ - humanReadableName: Longhorn - icon: PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxOTQuMjU2MTkgMTU5LjA4OTEyIj48dGl0bGU+aWNvbjwvdGl0bGU+PHBhdGggZmlsbD0iIzVmMjI0YSIgZD0iTTk3LjEyODEsODAuNjI2OWExMC4wMDksMTAuMDA5LDAsMCwxLTEuOTU4LS4xOTMzNmwtNS4yODM5NC0xLjA1NDkzLDQuNTQ4NTksMjcuOTgzMTVhOS45MjQyOSw5LjkyNDI5LDAsMCwxLC4xMjk4OCwxLjYwNDQ5aDUuMTI2YTkuOTk5Nyw5Ljk5OTcsMCwwLDEsLjE2MzA4LTEuNzk3MzZsNS4wOTgxNS0yNy45MDY0M0w5OS4wODYxLDgwLjQzMzU0QTEwLjAwODkyLDEwLjAwODkyLDAsMCwxLDk3LjEyODEsODAuNjI2OVoiLz48cGF0aCBmaWxsPSIjNWYyMjRhIiBkPSJNMTczLjY5NTYsMEgyMC41NjA3MUEyMC42MjEwNSwyMC42MjEwNSwwLDAsMCwwLDIwLjU2MDY3VjEzOC41Mjg0NWEyMC42MjEwNiwyMC42MjEwNiwwLDAsMCwyMC41NjA2NywyMC41NjA2N0gxNzMuNjk1NmEyMC42MjEsMjAuNjIxLDAsMCwwLDIwLjU2MDU1LTIwLjU2MDY3VjIwLjU2MDY3QTIwLjYyMSwyMC42MjEsMCwwLDAsMTczLjY5NTYsMFptMy40NTUsNDEuNTgxLTIuNTAyLDE3YTEwLjAwMDYsMTAuMDAwNiwwLDAsMS03LjkzNTU0LDguMzUwNThMMTI2LjA0NTQ1LDc1LjA1MWMtLjAwNy4wNDA3MS0uMDA4NTQuMDgxMDYtLjAxNi4xMjE3N2wtNi4zNjYyMSwzNC44NTA1OEEyMC4wMjY1LDIwLjAyNjUsMCwwLDEsOTkuNjkwNiwxMjguOTY2MjVoLTUuMTI2YTIwLjAyNTg3LDIwLjAyNTg3LDAsMCwxLTE5Ljk3ODUyLTE5LjA1ODFMNjguOTQ0NzUsNzUuMTk3NThsLTQxLjQwMTYyLTguMjY2QTEwLjAwMDYsMTAuMDAwNiwwLDAsMSwxOS42MDc1OSw1OC41ODFsLTIuNTAyLTE3YTEwLjAwMDEzLDEwLjAwMDEzLDAsMCwxLDE5Ljc4NzExLTIuOTEyMTFsMS40NzU1OSwxMC4wMjkzTDk3LjEyODEsNjAuNDI5NjNsNTguNzU5NzYtMTEuNzMxNDQsMS40NzU1OS0xMC4wMjkzQTEwLjAwMDEzLDEwLjAwMDEzLDAsMCwxLDE3Ny4xNTA1Niw0MS41ODFaIi8+PC9zdmc+Cg== - license: Apache-2.0 License - versions: - - version: 1.5.1 securitySchemes: oauth2Authentication: description: Operation requires OAuth2 bearer token authentication. diff --git a/pkg/openapi/types.go b/pkg/openapi/types.go index 5bd90850..09fcfe08 100644 --- a/pkg/openapi/types.go +++ b/pkg/openapi/types.go @@ -11,69 +11,6 @@ const ( Oauth2AuthenticationScopes = "oauth2Authentication.Scopes" ) -// ApplicationDependencies A set of applications that will be installed before this application. -type ApplicationDependencies = []ApplicationDependency - -// ApplicationDependency An application dependency. -type ApplicationDependency struct { - // Name The application name. - Name string `json:"name"` -} - -// ApplicationRead defines model for applicationRead. -type ApplicationRead struct { - // Metadata Resource metadata valid for all reads. - Metadata externalRef0.ResourceReadMetadata `json:"metadata"` - - // Spec An application. - Spec ApplicationSpec `json:"spec"` -} - -// ApplicationRecommends A set of recommended application that may be installed after this application. -type ApplicationRecommends = []ApplicationDependency - -// ApplicationSpec An application. -type ApplicationSpec struct { - // Documentation Documentation link for the application. - Documentation string `json:"documentation"` - - // HumanReadableName Human readable application name. - HumanReadableName string `json:"humanReadableName"` - - // Icon A base64 encoded SVG icon. This should work in both light and dark themes. - Icon []byte `json:"icon"` - - // License The license under which the application is released. - License string `json:"license"` - - // Tags A set of tags for filtering applications. - Tags *ApplicationTags `json:"tags,omitempty"` - - // Versions A set of application versions. - Versions ApplicationVersions `json:"versions"` -} - -// ApplicationTags A set of tags for filtering applications. -type ApplicationTags = []string - -// ApplicationVersion An application version. -type ApplicationVersion struct { - // Dependencies A set of applications that will be installed before this application. - Dependencies *ApplicationDependencies `json:"dependencies,omitempty"` - - // Recommends A set of recommended application that may be installed after this application. - Recommends *ApplicationRecommends `json:"recommends,omitempty"` - - // Version The application's Helm chart version. - Version string `json:"version"` -} - -// ApplicationVersions A set of application versions. -type ApplicationVersions = []ApplicationVersion - -// Applications A list of appications. -type Applications = []ApplicationRead - // ClusterManagerRead A cluster manager. type ClusterManagerRead struct { Metadata externalRef0.ProjectScopedResourceReadMetadata `json:"metadata"` @@ -183,9 +120,6 @@ type OrganizationIDParameter = KubernetesNameParameter // ProjectIDParameter A Kubernetes name. Must be a valid DNS containing only lower case characters, numbers or hyphens, start and end with a character or number, and be at most 63 characters in length. type ProjectIDParameter = KubernetesNameParameter -// ApplicationResponse A list of appications. -type ApplicationResponse = Applications - // ClusterManagerResponse A cluster manager. type ClusterManagerResponse = ClusterManagerRead diff --git a/pkg/server/handler/application/client.go b/pkg/server/handler/application/client.go deleted file mode 100644 index 66485aae..00000000 --- a/pkg/server/handler/application/client.go +++ /dev/null @@ -1,117 +0,0 @@ -/* -Copyright 2022-2024 EscherCloud. -Copyright 2024 the Unikorn Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package application - -import ( - "context" - "slices" - - unikornv1core "github.com/unikorn-cloud/core/pkg/apis/unikorn/v1alpha1" - coreopenapi "github.com/unikorn-cloud/core/pkg/openapi" - "github.com/unikorn-cloud/core/pkg/server/conversion" - "github.com/unikorn-cloud/core/pkg/server/errors" - "github.com/unikorn-cloud/kubernetes/pkg/openapi" - - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// Client wraps up application bundle related management handling. -type Client struct { - // client allows Kubernetes API access. - client client.Client -} - -// NewClient returns a new client with required parameters. -func NewClient(client client.Client) *Client { - return &Client{ - client: client, - } -} - -func convert(in *unikornv1core.HelmApplication) *openapi.ApplicationRead { - versions := make(openapi.ApplicationVersions, 0, len(in.Spec.Versions)) - - for _, version := range in.Spec.Versions { - v := openapi.ApplicationVersion{ - Version: *version.Version, - } - - if len(version.Dependencies) != 0 { - deps := make(openapi.ApplicationDependencies, 0, len(version.Dependencies)) - - for _, dependency := range version.Dependencies { - deps = append(deps, openapi.ApplicationDependency{ - Name: *dependency.Name, - }) - } - - v.Dependencies = &deps - } - - if len(version.Recommends) != 0 { - recommends := make(openapi.ApplicationRecommends, 0, len(version.Recommends)) - - for _, recommend := range version.Recommends { - recommends = append(recommends, openapi.ApplicationDependency{ - Name: *recommend.Name, - }) - } - - v.Recommends = &recommends - } - - versions = append(versions, v) - } - - out := &openapi.ApplicationRead{ - Metadata: conversion.ResourceReadMetadata(in, coreopenapi.ResourceProvisioningStatusProvisioned), - Spec: openapi.ApplicationSpec{ - Documentation: *in.Spec.Documentation, - License: *in.Spec.License, - Icon: in.Spec.Icon, - Versions: versions, - Tags: &in.Spec.Tags, - }, - } - - return out -} - -func convertList(in []unikornv1core.HelmApplication) []*openapi.ApplicationRead { - out := make([]*openapi.ApplicationRead, len(in)) - - for i := range in { - out[i] = convert(&in[i]) - } - - return out -} - -func (c *Client) List(ctx context.Context) ([]*openapi.ApplicationRead, error) { - result := &unikornv1core.HelmApplicationList{} - - if err := c.client.List(ctx, result); err != nil { - return nil, errors.OAuth2ServerError("failed to list applications").WithError(err) - } - - exported := result.Exported() - - slices.SortStableFunc(exported.Items, unikornv1core.CompareHelmApplication) - - return convertList(exported.Items), nil -} diff --git a/pkg/server/handler/handler.go b/pkg/server/handler/handler.go index d8c6f407..7abb8242 100644 --- a/pkg/server/handler/handler.go +++ b/pkg/server/handler/handler.go @@ -28,7 +28,6 @@ import ( identityapi "github.com/unikorn-cloud/identity/pkg/openapi" "github.com/unikorn-cloud/identity/pkg/rbac" "github.com/unikorn-cloud/kubernetes/pkg/openapi" - "github.com/unikorn-cloud/kubernetes/pkg/server/handler/application" "github.com/unikorn-cloud/kubernetes/pkg/server/handler/cluster" "github.com/unikorn-cloud/kubernetes/pkg/server/handler/clustermanager" regionclient "github.com/unikorn-cloud/region/pkg/client" @@ -271,14 +270,3 @@ func (h *Handler) GetApiV1OrganizationsOrganizationIDProjectsProjectIDClustersCl h.setUncacheable(w) util.WriteOctetStreamResponse(w, r, http.StatusOK, result) } - -func (h *Handler) GetApiV1OrganizationsOrganizationIDApplications(w http.ResponseWriter, r *http.Request, organizationID openapi.OrganizationIDParameter) { - result, err := application.NewClient(h.client).List(r.Context()) - if err != nil { - errors.HandleError(w, r, err) - return - } - - h.setUncacheable(w) - util.WriteJSONResponse(w, r, http.StatusOK, result) -}