247 lines
8.6 KiB
Go
247 lines
8.6 KiB
Go
// Copyright 2024 The Cozystack 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.
|
|
|
|
// Command openapi-gen assembles the OpenAPI v3 spec for apps.cozystack.io by
|
|
// spinning up an in-process Kubernetes API server with stub storage, so the
|
|
// framework generates the exact same paths and schemas as the real server.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
|
appsv1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1"
|
|
"github.com/cozystack/cozystack/pkg/apiserver"
|
|
cozyserver "github.com/cozystack/cozystack/pkg/cmd/server"
|
|
"github.com/cozystack/cozystack/pkg/config"
|
|
sampleopenapi "github.com/cozystack/cozystack/pkg/generated/openapi"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/watch"
|
|
"k8s.io/apiserver/pkg/endpoints/openapi"
|
|
"k8s.io/apiserver/pkg/registry/rest"
|
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
|
restclient "k8s.io/client-go/rest"
|
|
basecompatibility "k8s.io/component-base/compatibility"
|
|
baseversion "k8s.io/component-base/version"
|
|
"sigs.k8s.io/yaml"
|
|
)
|
|
|
|
func main() {
|
|
if err := run(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func run() error {
|
|
// Find all ApplicationDefinition YAML files
|
|
pattern := "packages/system/*-rd/cozyrds/*.yaml"
|
|
matches, err := filepath.Glob(pattern)
|
|
if err != nil {
|
|
return fmt.Errorf("glob %q: %w", pattern, err)
|
|
}
|
|
if len(matches) == 0 {
|
|
return fmt.Errorf("no files matched %q — run from repo root", pattern)
|
|
}
|
|
|
|
// Parse ApplicationDefinitions and build ResourceConfig
|
|
var resources []config.Resource
|
|
for _, path := range matches {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return fmt.Errorf("read %s: %w", path, err)
|
|
}
|
|
var appDef cozyv1alpha1.ApplicationDefinition
|
|
if err := yaml.Unmarshal(data, &appDef); err != nil {
|
|
return fmt.Errorf("parse %s: %w", path, err)
|
|
}
|
|
if appDef.Spec.Application.Kind == "" {
|
|
continue
|
|
}
|
|
resources = append(resources, config.Resource{
|
|
Application: config.ApplicationConfig{
|
|
Kind: appDef.Spec.Application.Kind,
|
|
Singular: appDef.Spec.Application.Singular,
|
|
Plural: appDef.Spec.Application.Plural,
|
|
OpenAPISchema: appDef.Spec.Application.OpenAPISchema,
|
|
},
|
|
})
|
|
}
|
|
|
|
if len(resources) == 0 {
|
|
return fmt.Errorf("no ApplicationDefinitions found")
|
|
}
|
|
|
|
resourceConfig := &config.ResourceConfig{Resources: resources}
|
|
|
|
// Register dynamic types in the apiserver scheme (same as the real server)
|
|
if err := appsv1alpha1.RegisterDynamicTypes(apiserver.Scheme, resourceConfig); err != nil {
|
|
return fmt.Errorf("register dynamic types: %w", err)
|
|
}
|
|
|
|
version := os.Getenv("VERSION")
|
|
if version == "" {
|
|
version = "dev"
|
|
}
|
|
|
|
// Create a minimal GenericAPIServer config
|
|
serverConfig := genericapiserver.NewConfig(apiserver.Codecs)
|
|
serverConfig.ExternalAddress = "localhost:443"
|
|
serverConfig.LoopbackClientConfig = &restclient.Config{}
|
|
serverConfig.FeatureGate = utilfeature.DefaultMutableFeatureGate
|
|
if baseversion.DefaultKubeBinaryVersion != "" {
|
|
serverConfig.EffectiveVersion = basecompatibility.NewEffectiveVersionFromString(baseversion.DefaultKubeBinaryVersion, "", "")
|
|
}
|
|
|
|
// OpenAPI v3 only — the v2 config is required by the framework but we only extract v3.
|
|
kindSchemas := cozyserver.KindSchemasFromConfig(resourceConfig)
|
|
serverConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(
|
|
sampleopenapi.GetOpenAPIDefinitions, openapi.NewDefinitionNamer(apiserver.Scheme),
|
|
)
|
|
serverConfig.OpenAPIV3Config = genericapiserver.DefaultOpenAPIV3Config(
|
|
sampleopenapi.GetOpenAPIDefinitions, openapi.NewDefinitionNamer(apiserver.Scheme),
|
|
)
|
|
serverConfig.OpenAPIV3Config.Info.Title = "Cozystack apps.cozystack.io API"
|
|
serverConfig.OpenAPIV3Config.Info.Version = version
|
|
serverConfig.OpenAPIV3Config.PostProcessSpec = cozyserver.BuildPostProcessV3(kindSchemas)
|
|
|
|
// Create the server
|
|
completed := serverConfig.Complete(nil)
|
|
server, err := completed.New("openapi-gen", genericapiserver.NewEmptyDelegate())
|
|
if err != nil {
|
|
return fmt.Errorf("create server: %w", err)
|
|
}
|
|
|
|
// Install apps API group with stub REST storage
|
|
appsStorage := map[string]rest.Storage{}
|
|
for _, res := range resourceConfig.Resources {
|
|
appsStorage[res.Application.Plural] = &stubREST{
|
|
gvk: schema.GroupVersion{
|
|
Group: "apps.cozystack.io",
|
|
Version: "v1alpha1",
|
|
}.WithKind(res.Application.Kind),
|
|
singularName: res.Application.Singular,
|
|
}
|
|
}
|
|
if err := apiserver.InstallAppsAPIGroup(server, appsStorage); err != nil {
|
|
return fmt.Errorf("install apps API group: %w", err)
|
|
}
|
|
|
|
// PrepareRun triggers OpenAPI spec generation
|
|
server.PrepareRun()
|
|
|
|
// Extract the v3 spec by hitting the server's HTTP handler directly
|
|
v3Path := "/openapi/v3/apis/apps.cozystack.io/v1alpha1"
|
|
req, err := http.NewRequest("GET", v3Path, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("create request: %w", err)
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
server.Handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
return fmt.Errorf("GET %s returned %d: %s", v3Path, rec.Code, rec.Body.String())
|
|
}
|
|
|
|
// Pretty-print the JSON
|
|
var raw json.RawMessage
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &raw); err != nil {
|
|
return fmt.Errorf("parse v3 response: %w", err)
|
|
}
|
|
|
|
enc := json.NewEncoder(os.Stdout)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(raw)
|
|
}
|
|
|
|
// stubREST implements the same REST interfaces as the real application.REST
|
|
// but never handles actual requests. It exists solely so the K8s framework
|
|
// generates the correct OpenAPI paths and schemas.
|
|
type stubREST struct {
|
|
gvk schema.GroupVersionKind
|
|
singularName string
|
|
}
|
|
|
|
// Compile-time interface checks — same set as the real application.REST.
|
|
var (
|
|
_ rest.Getter = &stubREST{}
|
|
_ rest.Lister = &stubREST{}
|
|
_ rest.Creater = &stubREST{}
|
|
_ rest.Updater = &stubREST{}
|
|
_ rest.Patcher = &stubREST{}
|
|
_ rest.GracefulDeleter = &stubREST{}
|
|
_ rest.Watcher = &stubREST{}
|
|
)
|
|
|
|
func (s *stubREST) New() runtime.Object {
|
|
obj := &appsv1alpha1.Application{}
|
|
obj.TypeMeta = metav1.TypeMeta{
|
|
APIVersion: s.gvk.GroupVersion().String(),
|
|
Kind: s.gvk.Kind,
|
|
}
|
|
return obj
|
|
}
|
|
|
|
func (s *stubREST) NewList() runtime.Object {
|
|
obj := &appsv1alpha1.ApplicationList{}
|
|
obj.TypeMeta = metav1.TypeMeta{
|
|
APIVersion: s.gvk.GroupVersion().String(),
|
|
Kind: s.gvk.Kind + "List",
|
|
}
|
|
return obj
|
|
}
|
|
|
|
func (s *stubREST) Destroy() {}
|
|
func (s *stubREST) NamespaceScoped() bool { return true }
|
|
func (s *stubREST) GetSingularName() string { return s.singularName }
|
|
func (s *stubREST) GroupVersionKind(schema.GroupVersion) schema.GroupVersionKind { return s.gvk }
|
|
|
|
func (s *stubREST) Get(_ context.Context, _ string, _ *metav1.GetOptions) (runtime.Object, error) {
|
|
return nil, fmt.Errorf("stub: not implemented")
|
|
}
|
|
|
|
func (s *stubREST) List(_ context.Context, _ *metainternalversion.ListOptions) (runtime.Object, error) {
|
|
return nil, fmt.Errorf("stub: not implemented")
|
|
}
|
|
|
|
func (s *stubREST) Create(_ context.Context, _ runtime.Object, _ rest.ValidateObjectFunc, _ *metav1.CreateOptions) (runtime.Object, error) {
|
|
return nil, fmt.Errorf("stub: not implemented")
|
|
}
|
|
|
|
func (s *stubREST) Update(_ context.Context, _ string, _ rest.UpdatedObjectInfo, _ rest.ValidateObjectFunc, _ rest.ValidateObjectUpdateFunc, _ bool, _ *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
|
return nil, false, fmt.Errorf("stub: not implemented")
|
|
}
|
|
|
|
func (s *stubREST) Delete(_ context.Context, _ string, _ rest.ValidateObjectFunc, _ *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
|
return nil, false, fmt.Errorf("stub: not implemented")
|
|
}
|
|
|
|
func (s *stubREST) Watch(_ context.Context, _ *metainternalversion.ListOptions) (watch.Interface, error) {
|
|
return nil, fmt.Errorf("stub: not implemented")
|
|
}
|
|
|
|
func (s *stubREST) ConvertToTable(_ context.Context, _ runtime.Object, _ runtime.Object) (*metav1.Table, error) {
|
|
return nil, fmt.Errorf("stub: not implemented")
|
|
}
|