cozystack/tools/openapi-gen/main.go
Myasnikov Daniil 9ef0e2ceeb
[docs] Added openapi generation tool
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-03-25 15:57:25 +05:00

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")
}