[docs] Added openapi generation tool
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
This commit is contained in:
parent
e090e2f317
commit
9ef0e2ceeb
6 changed files with 302 additions and 29 deletions
6
Makefile
6
Makefile
|
|
@ -58,7 +58,11 @@ manifests:
|
|||
cozypkg:
|
||||
go build -ldflags "-X github.com/cozystack/cozystack/cmd/cozypkg/cmd.Version=v$(COZYSTACK_VERSION)" -o _out/bin/cozypkg ./cmd/cozypkg
|
||||
|
||||
assets: assets-talos assets-cozypkg
|
||||
assets: assets-talos assets-cozypkg openapi-json
|
||||
|
||||
openapi-json:
|
||||
mkdir -p _out/assets
|
||||
VERSION=$(shell git describe --tags --always 2>/dev/null || echo dev) go run ./tools/openapi-gen/ 2>/dev/null > _out/assets/openapi.json
|
||||
|
||||
assets-talos:
|
||||
make -C packages/core/talos assets
|
||||
|
|
|
|||
|
|
@ -14,3 +14,4 @@ gh release upload --clobber $version _out/assets/kernel-amd64
|
|||
gh release upload --clobber $version _out/assets/initramfs-metal-amd64.xz
|
||||
gh release upload --clobber $version _out/assets/cozypkg-*.tar.gz
|
||||
gh release upload --clobber $version _out/assets/cozypkg-checksums.txt
|
||||
gh release upload --clobber $version _out/assets/openapi.json
|
||||
|
|
|
|||
|
|
@ -211,15 +211,21 @@ func (c completedConfig) New() (*CozyServer, error) {
|
|||
storage := applicationstorage.NewREST(cli, watchCli, &resConfig)
|
||||
appsV1alpha1Storage[resConfig.Application.Plural] = cozyregistry.RESTInPeace(storage)
|
||||
}
|
||||
appsApiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apps.GroupName, Scheme, metav1.ParameterCodec, Codecs)
|
||||
appsApiGroupInfo.VersionedResourcesStorageMap["v1alpha1"] = appsV1alpha1Storage
|
||||
if err := s.GenericAPIServer.InstallAPIGroup(&appsApiGroupInfo); err != nil {
|
||||
if err := InstallAppsAPIGroup(s.GenericAPIServer, appsV1alpha1Storage); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// InstallAppsAPIGroup registers the apps.cozystack.io API group on the given
|
||||
// server using the provided storage map (plural name → rest.Storage).
|
||||
func InstallAppsAPIGroup(server *genericapiserver.GenericAPIServer, storage map[string]rest.Storage) error {
|
||||
info := genericapiserver.NewDefaultAPIGroupInfo(apps.GroupName, Scheme, metav1.ParameterCodec, Codecs)
|
||||
info.VersionedResourcesStorageMap["v1alpha1"] = storage
|
||||
return server.InstallAPIGroup(&info)
|
||||
}
|
||||
|
||||
func mustGetInformers(ctx context.Context, mgr ctrl.Manager, types ...client.Object) error {
|
||||
for i := range types {
|
||||
if _, err := mgr.GetCache().GetInformer(ctx, types[i]); err != nil {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cozystack/cozystack/pkg/apiserver"
|
||||
"github.com/cozystack/cozystack/pkg/config"
|
||||
sampleopenapi "github.com/cozystack/cozystack/pkg/generated/openapi"
|
||||
"k8s.io/apiserver/pkg/endpoints/openapi"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
)
|
||||
|
|
@ -210,7 +215,9 @@ func rewriteRefForKind(old, kind string) string {
|
|||
// -----------------------------------------------------------------------------
|
||||
// OpenAPI **v3** post-processor
|
||||
// -----------------------------------------------------------------------------
|
||||
func buildPostProcessV3(kindSchemas map[string]string) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) {
|
||||
// BuildPostProcessV3 returns an OpenAPI v3 post-processor that clones base
|
||||
// Application schemas into per-kind schemas and rewrites $ref pointers.
|
||||
func BuildPostProcessV3(kindSchemas map[string]string) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) {
|
||||
return func(doc *spec3.OpenAPI) (*spec3.OpenAPI, error) {
|
||||
|
||||
if doc.Components == nil {
|
||||
|
|
@ -333,7 +340,36 @@ func sanitizeForV2(s *spec.Schema) {
|
|||
// -----------------------------------------------------------------------------
|
||||
// OpenAPI **v2** (swagger) post-processor
|
||||
// -----------------------------------------------------------------------------
|
||||
func buildPostProcessV2(kindSchemas map[string]string) func(*spec.Swagger) (*spec.Swagger, error) {
|
||||
// BuildPostProcessV2 returns a Swagger post-processor that clones base
|
||||
// Application schemas into per-kind schemas and rewrites $ref pointers.
|
||||
// KindSchemasFromConfig extracts the kind→OpenAPISchema mapping from a ResourceConfig.
|
||||
func KindSchemasFromConfig(rc *config.ResourceConfig) map[string]string {
|
||||
m := make(map[string]string, len(rc.Resources))
|
||||
for _, r := range rc.Resources {
|
||||
m[r.Application.Kind] = r.Application.OpenAPISchema
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// ConfigureOpenAPI sets up OpenAPI v2 and v3 on a GenericAPIServer Config,
|
||||
// including the post-processors that clone Application schemas to per-kind schemas.
|
||||
func ConfigureOpenAPI(cfg *genericapiserver.Config, kindSchemas map[string]string, title, version string) {
|
||||
cfg.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(
|
||||
sampleopenapi.GetOpenAPIDefinitions, openapi.NewDefinitionNamer(apiserver.Scheme),
|
||||
)
|
||||
cfg.OpenAPIConfig.Info.Title = title
|
||||
cfg.OpenAPIConfig.Info.Version = version
|
||||
cfg.OpenAPIConfig.PostProcessSpec = BuildPostProcessV2(kindSchemas)
|
||||
|
||||
cfg.OpenAPIV3Config = genericapiserver.DefaultOpenAPIV3Config(
|
||||
sampleopenapi.GetOpenAPIDefinitions, openapi.NewDefinitionNamer(apiserver.Scheme),
|
||||
)
|
||||
cfg.OpenAPIV3Config.Info.Title = title
|
||||
cfg.OpenAPIV3Config.Info.Version = version
|
||||
cfg.OpenAPIV3Config.PostProcessSpec = BuildPostProcessV3(kindSchemas)
|
||||
}
|
||||
|
||||
func BuildPostProcessV2(kindSchemas map[string]string) func(*spec.Swagger) (*spec.Swagger, error) {
|
||||
return func(sw *spec.Swagger) (*spec.Swagger, error) {
|
||||
defs := sw.Definitions
|
||||
base, ok1 := defs[baseRef]
|
||||
|
|
|
|||
|
|
@ -31,12 +31,10 @@ import (
|
|||
corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1"
|
||||
"github.com/cozystack/cozystack/pkg/apiserver"
|
||||
"github.com/cozystack/cozystack/pkg/config"
|
||||
sampleopenapi "github.com/cozystack/cozystack/pkg/generated/openapi"
|
||||
"github.com/spf13/cobra"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apiserver/pkg/endpoints/openapi"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
genericoptions "k8s.io/apiserver/pkg/server/options"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
|
|
@ -213,10 +211,6 @@ func (o *CozyServerOptions) Config() (*apiserver.Config, error) {
|
|||
|
||||
serverConfig := genericapiserver.NewRecommendedConfig(apiserver.Codecs)
|
||||
|
||||
serverConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(
|
||||
sampleopenapi.GetOpenAPIDefinitions, openapi.NewDefinitionNamer(apiserver.Scheme),
|
||||
)
|
||||
|
||||
apiVersion := "0.1"
|
||||
if o.ResourceConfig != nil {
|
||||
raw, err := json.Marshal(o.ResourceConfig)
|
||||
|
|
@ -227,23 +221,8 @@ func (o *CozyServerOptions) Config() (*apiserver.Config, error) {
|
|||
apiVersion = "0.1-" + hex.EncodeToString(sum[:8])
|
||||
}
|
||||
|
||||
// capture schemas from config once for fast lookup inside the closure
|
||||
kindSchemas := map[string]string{}
|
||||
for _, r := range o.ResourceConfig.Resources {
|
||||
kindSchemas[r.Application.Kind] = r.Application.OpenAPISchema
|
||||
}
|
||||
|
||||
serverConfig.OpenAPIConfig.Info.Title = "Cozy"
|
||||
serverConfig.OpenAPIConfig.Info.Version = apiVersion
|
||||
serverConfig.OpenAPIConfig.PostProcessSpec = buildPostProcessV2(kindSchemas)
|
||||
|
||||
serverConfig.OpenAPIV3Config = genericapiserver.DefaultOpenAPIV3Config(
|
||||
sampleopenapi.GetOpenAPIDefinitions, openapi.NewDefinitionNamer(apiserver.Scheme),
|
||||
)
|
||||
serverConfig.OpenAPIV3Config.Info.Title = "Cozy"
|
||||
serverConfig.OpenAPIV3Config.Info.Version = apiVersion
|
||||
|
||||
serverConfig.OpenAPIV3Config.PostProcessSpec = buildPostProcessV3(kindSchemas)
|
||||
kindSchemas := KindSchemasFromConfig(o.ResourceConfig)
|
||||
ConfigureOpenAPI(&serverConfig.Config, kindSchemas, "Cozy", apiVersion)
|
||||
|
||||
// Set FeatureGate and EffectiveVersion - required for Complete() in Kubernetes v0.34.1
|
||||
// Following the pattern from sample-apiserver, but creating EffectiveVersion directly
|
||||
|
|
|
|||
247
tools/openapi-gen/main.go
Normal file
247
tools/openapi-gen/main.go
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
// 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")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue