174 lines
5.2 KiB
Go
174 lines
5.2 KiB
Go
// Package crds is used for installing rancher CRDs.
|
|
package crds
|
|
|
|
import (
|
|
"context"
|
|
"embed"
|
|
"fmt"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/rancher/rancher/pkg/fleet"
|
|
"github.com/rancher/wrangler/v3/pkg/crd"
|
|
"github.com/rancher/wrangler/v3/pkg/yaml"
|
|
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
|
clientv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
"k8s.io/apimachinery/pkg/selection"
|
|
)
|
|
|
|
const (
|
|
baseDir = "."
|
|
|
|
// readyDuration time to wait for CRDs to be ready.
|
|
readyDuration = time.Minute * 1
|
|
|
|
k8sManagedByKey = "app.kubernetes.io/managed-by"
|
|
managerValue = "rancher"
|
|
crdKind = "CustomResourceDefinition"
|
|
)
|
|
|
|
var (
|
|
//go:embed yaml
|
|
crdFS embed.FS
|
|
|
|
errDuplicate = fmt.Errorf("duplicate CRD")
|
|
)
|
|
|
|
// EnsureRequired will ensure all required CRDs needed by Rancher based on the currently enabled features are installed and up to date.
|
|
func EnsureRequired(ctx context.Context, crdClient clientv1.CustomResourceDefinitionInterface) error {
|
|
return Ensure(ctx, crdClient, RequiredCRDs())
|
|
}
|
|
|
|
// Ensure will ensure all given CRD names are installed and up to date.
|
|
// Ensure looks for matching crd names inside the embedded directory 'yaml'.
|
|
func Ensure(ctx context.Context, crdClient clientv1.CustomResourceDefinitionInterface, crdNames []string) error {
|
|
toCreateCRDs, err := getCRDs(crdNames)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get CRDs: %w", err)
|
|
}
|
|
|
|
ownedByRancher, err := labels.NewRequirement(k8sManagedByKey, selection.Equals, []string{managerValue})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create crd label selector: %w", err)
|
|
}
|
|
selector := labels.NewSelector().Add(*ownedByRancher)
|
|
|
|
err = crd.BatchCreateCRDs(ctx, crdClient, selector, readyDuration, toCreateCRDs)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create CRDs: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getCRDs finds all embedded CRDs that match the given list of CRDs. If a CRD is specified and not found an error is returned.
|
|
func getCRDs(crdNames []string) ([]*apiextv1.CustomResourceDefinition, error) {
|
|
allCRDs, err := crdsFromDir(baseDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
capiMap := toMap(CAPICRDs())
|
|
bootstrapFleetMap := toMap(bootstrapFleet())
|
|
|
|
retCRDs := []*apiextv1.CustomResourceDefinition{}
|
|
|
|
for _, crdName := range crdNames {
|
|
// only return CRDs that are specified and have been migrated to the new CRD generation flow
|
|
if !MigratedResources[crdName] {
|
|
continue
|
|
}
|
|
|
|
crd, found := allCRDs[crdName]
|
|
if !found {
|
|
return nil, fmt.Errorf("CRD yaml '%s' not found in embedded file system", crdName)
|
|
}
|
|
|
|
if crd.Labels == nil {
|
|
crd.Labels = map[string]string{}
|
|
}
|
|
|
|
// dynamically add labels to capi crds since we do not generate them.
|
|
if capiMap[crd.Name] {
|
|
crd.Labels["auth.cattle.io/cluster-indexed"] = "true"
|
|
}
|
|
|
|
// Add managed by label
|
|
if bootstrapFleetMap[crd.Name] {
|
|
// Ensure labels/annotations are set so that helm will manage this
|
|
crd.Labels[k8sManagedByKey] = "Helm"
|
|
if crd.Annotations == nil {
|
|
crd.Annotations = map[string]string{}
|
|
}
|
|
crd.Annotations["meta.helm.sh/release-name"] = fleet.CRDChartName
|
|
crd.Annotations["meta.helm.sh/release-namespace"] = fleet.ReleaseNamespace
|
|
} else {
|
|
// since this CRD is installed by rancher add the manged by label
|
|
crd.Labels[k8sManagedByKey] = managerValue
|
|
}
|
|
|
|
retCRDs = append(retCRDs, crd)
|
|
}
|
|
|
|
return retCRDs, nil
|
|
}
|
|
|
|
// crdsFromDir recursively traverses the embedded yaml directory and find all CRD yamls.
|
|
func crdsFromDir(dirName string) (map[string]*apiextv1.CustomResourceDefinition, error) {
|
|
// read all entries in the embedded directory
|
|
crdFiles, err := crdFS.ReadDir(dirName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read embedded dir '%s': %w", dirName, err)
|
|
}
|
|
|
|
allCRDs := map[string]*apiextv1.CustomResourceDefinition{}
|
|
for _, dirEntry := range crdFiles {
|
|
fullPath := filepath.Join(dirName, dirEntry.Name())
|
|
if dirEntry.IsDir() {
|
|
// if the entry is the dir recurse into that folder to get all crds
|
|
subCRDs, err := crdsFromDir(fullPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for k, v := range subCRDs {
|
|
if _, ok := allCRDs[k]; ok {
|
|
return nil, fmt.Errorf("%w for '%s", errDuplicate, k)
|
|
}
|
|
allCRDs[k] = v
|
|
}
|
|
continue
|
|
}
|
|
|
|
// read the file and convert it to a crd object
|
|
file, err := crdFS.Open(fullPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open embedded file '%s': %w", fullPath, err)
|
|
}
|
|
crdObjs, err := yaml.UnmarshalWithJSONDecoder[*apiextv1.CustomResourceDefinition](file)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert embedded file '%s' to yaml: %w", fullPath, err)
|
|
}
|
|
for _, crdObj := range crdObjs {
|
|
if crdObj.Kind != crdKind {
|
|
// if the yaml is not a CRD return an error
|
|
return nil, fmt.Errorf("decoded object is not '%s' instead found Kind='%s'", crdKind, crdObj.Kind)
|
|
}
|
|
if _, ok := allCRDs[crdObj.Name]; ok {
|
|
return nil, fmt.Errorf("%w for '%s", errDuplicate, crdObj.Name)
|
|
}
|
|
allCRDs[crdObj.Name] = crdObj
|
|
}
|
|
}
|
|
return allCRDs, nil
|
|
}
|
|
|
|
// toMap converts a list of slice to a map.
|
|
func toMap(strings []string) map[string]bool {
|
|
retMap := make(map[string]bool, len(strings))
|
|
for _, name := range strings {
|
|
retMap[name] = true
|
|
}
|
|
return retMap
|
|
}
|