cozystack/pkg/cmd/server/start.go
Aleksei Sviridkin e267cfcf9d
fix(api): address review feedback for validation consistency
- Return field.ErrorList from validateNameLength for consistent
  apierrors.NewInvalid error shape (was NewBadRequest)
- Add klog warning when YAML parsing fails in parseRootHostFromSecret
- Fix maxHelmReleaseName comment to accurately describe Helm convention
- Add note that root-host changes require API server restart
- Replace interface{} with any throughout openapi.go and rest.go
- Remove trailing blank line in const block

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-02-11 13:29:42 +03:00

328 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
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.
*/
package server
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net"
"time"
v1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
appsv1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1"
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"
basecompatibility "k8s.io/component-base/compatibility"
baseversion "k8s.io/component-base/version"
"k8s.io/klog/v2"
netutils "k8s.io/utils/net"
"sigs.k8s.io/controller-runtime/pkg/client"
k8sconfig "sigs.k8s.io/controller-runtime/pkg/client/config"
"sigs.k8s.io/yaml"
)
// CozyServerOptions holds the state for the Cozy API server
type CozyServerOptions struct {
RecommendedOptions *genericoptions.RecommendedOptions
StdOut io.Writer
StdErr io.Writer
AlternateDNS []string
Client client.Client
// Add a field to store the configuration
ResourceConfig *config.ResourceConfig
}
// NewCozyServerOptions returns a new instance of CozyServerOptions
func NewCozyServerOptions(out, errOut io.Writer) *CozyServerOptions {
o := &CozyServerOptions{
RecommendedOptions: genericoptions.NewRecommendedOptions(
"",
apiserver.Codecs.LegacyCodec(
corev1alpha1.SchemeGroupVersion,
appsv1alpha1.SchemeGroupVersion,
),
),
StdOut: out,
StdErr: errOut,
}
o.RecommendedOptions.Etcd = nil
return o
}
// NewCommandStartCozyServer provides a CLI handler for the 'start apps-server' command
func NewCommandStartCozyServer(ctx context.Context, defaults *CozyServerOptions) *cobra.Command {
o := *defaults
cmd := &cobra.Command{
Short: "Launch an Cozystack API server",
Long: "Launch an Cozystack API server",
RunE: func(c *cobra.Command, args []string) error {
if err := o.Complete(); err != nil {
return err
}
if err := o.Validate(args); err != nil {
return err
}
if err := o.RunCozyServer(c.Context()); err != nil {
return err
}
return nil
},
}
cmd.SetContext(ctx)
flags := cmd.Flags()
o.RecommendedOptions.AddFlags(flags)
// Note: KEP-4330 component versioning functionality (k8s.io/apiserver/pkg/util/version)
// is not available in Kubernetes v0.34.1. The component versioning code has been removed.
return cmd
}
// Complete fills in the fields that are not set
func (o *CozyServerOptions) Complete() error {
scheme := runtime.NewScheme()
if err := v1alpha1.AddToScheme(scheme); err != nil {
return fmt.Errorf("failed to register types: %w", err)
}
if err := corev1.AddToScheme(scheme); err != nil {
return fmt.Errorf("failed to register core types: %w", err)
}
cfg, err := k8sconfig.GetConfig()
if err != nil {
return fmt.Errorf("failed to get kubeconfig: %w", err)
}
o.Client, err = client.New(cfg, client.Options{Scheme: scheme})
if err != nil {
return fmt.Errorf("client initialization failed: %w", err)
}
crdList := &v1alpha1.ApplicationDefinitionList{}
// Retry with exponential backoff for at least 30 minutes
const maxRetryDuration = 30 * time.Minute
const initialDelay = time.Second
const maxDelay = 2 * time.Minute
startTime := time.Now()
delay := initialDelay
for {
err := o.Client.List(context.Background(), crdList)
if err == nil {
break
}
// Check if we've exceeded the maximum retry duration
if time.Since(startTime) >= maxRetryDuration {
return fmt.Errorf("failed to list ApplicationDefinitions after %v: %w", maxRetryDuration, err)
}
// Log the error and wait before retrying
fmt.Printf("Failed to list ApplicationDefinitions (retrying in %v): %v\n", delay, err)
time.Sleep(delay)
delay = time.Duration(float64(delay) * 1.5)
if delay > maxDelay {
delay = maxDelay
}
}
// Convert to ResourceConfig
o.ResourceConfig = &config.ResourceConfig{}
for _, crd := range crdList.Items {
resource := config.Resource{
Application: config.ApplicationConfig{
Kind: crd.Spec.Application.Kind,
Singular: crd.Spec.Application.Singular,
Plural: crd.Spec.Application.Plural,
ShortNames: []string{}, // TODO: implement shortnames
OpenAPISchema: crd.Spec.Application.OpenAPISchema,
},
Release: config.ReleaseConfig{
Prefix: crd.Spec.Release.Prefix,
Labels: crd.Spec.Release.Labels,
ChartRef: config.ChartRefConfig{
Kind: crd.Spec.Release.ChartRef.Kind,
Name: crd.Spec.Release.ChartRef.Name,
Namespace: crd.Spec.Release.ChartRef.Namespace,
},
},
}
o.ResourceConfig.Resources = append(o.ResourceConfig.Resources, resource)
}
// Read root-host from cozystack-values secret (best-effort, non-fatal)
secret := &corev1.Secret{}
err = o.Client.Get(context.Background(), client.ObjectKey{
Namespace: "cozy-system",
Name: "cozystack-values",
}, secret)
if err != nil {
klog.Warningf("failed to read cozystack-values secret: %v", err)
} else {
o.ResourceConfig.RootHost = parseRootHostFromSecret(secret)
if o.ResourceConfig.RootHost != "" {
klog.Infof("Loaded root-host: %s", o.ResourceConfig.RootHost)
}
}
return nil
}
// parseRootHostFromSecret extracts _cluster.root-host from the cozystack-values secret.
func parseRootHostFromSecret(secret *corev1.Secret) string {
if secret == nil || secret.Data == nil {
return ""
}
valuesYAML, ok := secret.Data["values.yaml"]
if !ok || len(valuesYAML) == 0 {
return ""
}
var values struct {
Cluster struct {
RootHost string `json:"root-host"`
} `json:"_cluster"`
}
if err := yaml.Unmarshal(valuesYAML, &values); err != nil {
klog.Warningf("failed to parse values.yaml from cozystack-values secret: %v", err)
return ""
}
return values.Cluster.RootHost
}
// Validate checks the correctness of the options
func (o CozyServerOptions) Validate(args []string) error {
var allErrors []error
allErrors = append(allErrors, o.RecommendedOptions.Validate()...)
return utilerrors.NewAggregate(allErrors)
}
// Config returns the configuration for the API server based on CozyServerOptions
func (o *CozyServerOptions) Config() (*apiserver.Config, error) {
// TODO: set the "real" external address
if err := o.RecommendedOptions.SecureServing.MaybeDefaultWithSelfSignedCerts(
"localhost", o.AlternateDNS, []net.IP{netutils.ParseIPSloppy("127.0.0.1")},
); err != nil {
return nil, fmt.Errorf("error creating self-signed certificates: %v", err)
}
// Register *compile-time* resources first.
corev1alpha1.RegisterStaticTypes(apiserver.Scheme)
// Register *run-time* resources (from the users config file).
err := appsv1alpha1.RegisterDynamicTypes(apiserver.Scheme, o.ResourceConfig)
if err != nil {
return nil, fmt.Errorf("failed to register dynamic types: %v", err)
}
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)
if err != nil {
return nil, fmt.Errorf("failed to marshal resource config: %v", err)
}
sum := sha256.Sum256(raw)
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)
// Set FeatureGate and EffectiveVersion - required for Complete() in Kubernetes v0.34.1
// Following the pattern from sample-apiserver, but creating EffectiveVersion directly
// without ComponentGlobalsRegistry
serverConfig.FeatureGate = utilfeature.DefaultMutableFeatureGate
// Create EffectiveVersion directly using compatibility package
// This is needed even without ComponentGlobalsRegistry
if baseversion.DefaultKubeBinaryVersion != "" {
serverConfig.EffectiveVersion = basecompatibility.NewEffectiveVersionFromString(baseversion.DefaultKubeBinaryVersion, "", "")
}
if err := o.RecommendedOptions.ApplyTo(serverConfig); err != nil {
return nil, err
}
config := &apiserver.Config{
GenericConfig: serverConfig,
ResourceConfig: o.ResourceConfig,
}
return config, nil
}
// RunCozyServer launches a new CozyServer based on CozyServerOptions
func (o CozyServerOptions) RunCozyServer(ctx context.Context) error {
config, err := o.Config()
if err != nil {
return err
}
server, err := config.Complete().New()
if err != nil {
return err
}
server.GenericAPIServer.AddPostStartHookOrDie("start-sample-server-informers", func(context genericapiserver.PostStartHookContext) error {
config.GenericConfig.SharedInformerFactory.Start(context.Done())
return nil
})
return server.GenericAPIServer.PrepareRun().RunWithContext(ctx)
}