package environment_groups import ( "context" "fmt" "strconv" "strings" "time" "github.com/porter-dev/porter/internal/kubernetes" "github.com/porter-dev/porter/internal/telemetry" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ( LabelKey_LinkedEnvironmentGroup = "porter.run/linked-environment-group" LabelKey_EnvironmentGroupVersion = "porter.run/environment-group-version" LabelKey_EnvironmentGroupName = "porter.run/environment-group-name" // Namespace_EnvironmentGroups is the base namespace for storing all environment groups. // The configmaps and secrets here should be considered the source's of truth for a given version Namespace_EnvironmentGroups = "porter-env-group" ) // EnvironmentGroup represents a ConfigMap in the porter-env-group namespace type EnvironmentGroup struct { // Name is the environment group name which can be found in the labels (LabelKey_EnvironmentGroupName) of the ConfigMap. This is NOT the configmap name Name string `json:"name"` // Version is the environment group version which can be found in the labels (LabelKey_EnvironmentGroupVersion) of the ConfigMap. This is NOT included in the configmap name Version int `json:"latest_version"` // Variables are non-secret values for the EnvironmentGroup. This usually will be a configmap Variables map[string]string `json:"variables"` // SecretVariables are secret values for the EnvironmentGroup. This usually will be a Secret on the kubernetes cluster SecretVariables map[string][]byte `json:"variables_secrets"` // CreatedAt is only used for display purposes and is in UTC Unix time CreatedAtUTC time.Time `json:"created_at"` } type environmentGroupOptions struct { namespace string environmentGroupLabelName string environmentGroupLabelVersion int } // EnvironmentGroupOption is a function that modifies ListEnvironmentGroups type EnvironmentGroupOption func(*environmentGroupOptions) // WithNamespace filters all environment groups in a given namespace func WithNamespace(namespace string) EnvironmentGroupOption { return func(opts *environmentGroupOptions) { opts.namespace = namespace } } // WithEnvironmentGroupName filters all environment groups by name func WithEnvironmentGroupName(name string) EnvironmentGroupOption { return func(opts *environmentGroupOptions) { opts.environmentGroupLabelName = name } } // WithEnvironmentGroupVersion filters all environment groups by version func WithEnvironmentGroupVersion(version int) EnvironmentGroupOption { return func(opts *environmentGroupOptions) { opts.environmentGroupLabelVersion = version } } // ListEnvironmentGroups returns all environment groups stored in the provided namespace. If none is set, it will use the namespace "porter-env-group" func ListEnvironmentGroups(ctx context.Context, a *kubernetes.Agent, listOpts ...EnvironmentGroupOption) ([]EnvironmentGroup, error) { ctx, span := telemetry.NewSpan(ctx, "list-environment-groups") defer span.End() var opts environmentGroupOptions for _, opt := range listOpts { opt(&opts) } if opts.namespace == "" { opts.namespace = Namespace_EnvironmentGroups } var labelSelectors []string if opts.environmentGroupLabelName != "" { labelSelectors = append(labelSelectors, fmt.Sprintf("%s=%s", LabelKey_EnvironmentGroupName, opts.environmentGroupLabelName)) } if opts.environmentGroupLabelVersion != 0 { labelSelectors = append(labelSelectors, fmt.Sprintf("%s=%d", LabelKey_EnvironmentGroupVersion, opts.environmentGroupLabelVersion)) } labelSelector := strings.Join(labelSelectors, ",") listOptions := metav1.ListOptions{ LabelSelector: labelSelector, } telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "namespace", Value: opts.namespace}, telemetry.AttributeKV{Key: "label-selector", Value: labelSelector}, ) configMapListResp, err := a.Clientset.CoreV1().ConfigMaps(opts.namespace).List(ctx, listOptions) if err != nil { return nil, telemetry.Error(ctx, span, err, "unable to list environment group variables") } secretListResp, err := a.Clientset.CoreV1().Secrets(opts.namespace).List(ctx, listOptions) if err != nil { return nil, telemetry.Error(ctx, span, err, "unable to list environment groups secret varialbes") } // envGroupSet's key is the environment group's versioned name envGroupSet := make(map[string]EnvironmentGroup) for _, cm := range configMapListResp.Items { name, ok := cm.Labels[LabelKey_EnvironmentGroupName] if !ok { continue // missing name label, not an environment group } versionString, ok := cm.Labels[LabelKey_EnvironmentGroupVersion] if !ok { continue // missing version label, not an environment group } version, err := strconv.Atoi(versionString) if err != nil { continue // invalid version label as it should be an int, not an environment group } if _, ok := envGroupSet[cm.Name]; !ok { envGroupSet[cm.Name] = EnvironmentGroup{} } envGroupSet[cm.Name] = EnvironmentGroup{ Name: name, Version: version, Variables: cm.Data, SecretVariables: envGroupSet[cm.Name].SecretVariables, CreatedAtUTC: cm.CreationTimestamp.Time.UTC(), } } for _, secret := range secretListResp.Items { name, ok := secret.Labels[LabelKey_EnvironmentGroupName] if !ok { continue // missing name label, not an environment group } versionString, ok := secret.Labels[LabelKey_EnvironmentGroupVersion] if !ok { continue // missing version label, not an environment group } version, err := strconv.Atoi(versionString) if err != nil { continue // invalid version label as it should be an int, not an environment group } if _, ok := envGroupSet[secret.Name]; !ok { envGroupSet[secret.Name] = EnvironmentGroup{} } envGroupSet[secret.Name] = EnvironmentGroup{ Name: name, Version: version, SecretVariables: secret.Data, Variables: envGroupSet[secret.Name].Variables, CreatedAtUTC: secret.CreationTimestamp.Time.UTC(), } } var envGroups []EnvironmentGroup for _, envGroup := range envGroupSet { envGroups = append(envGroups, envGroup) } return envGroups, nil } // LinkedPorterApplication represents an application which was linked to an environment group type LinkedPorterApplication struct { Name string Namespace string } // LinkedApplications lists all applications that are linked to a given environment group. Since there can be multiple linked environment groups we must check by the presence of a label on the deployment and job func LinkedApplications(ctx context.Context, a *kubernetes.Agent, environmentGroupName string) ([]LinkedPorterApplication, error) { ctx, span := telemetry.NewSpan(ctx, "list-linked-applications") defer span.End() if environmentGroupName == "" { return nil, telemetry.Error(ctx, span, nil, "environment group cannot be empty") } telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "environment-group-name", Value: environmentGroupName}) deployListResp, err := a.Clientset.AppsV1().Deployments(metav1.NamespaceAll).List(ctx, metav1.ListOptions{ LabelSelector: LabelKey_LinkedEnvironmentGroup, }) if err != nil { return nil, telemetry.Error(ctx, span, err, "unable to list linked deployment applications") } var apps []LinkedPorterApplication for _, d := range deployListResp.Items { applicationsLinkedEnvironmentGroups := strings.Split(d.Labels[LabelKey_LinkedEnvironmentGroup], ".") for _, linkedEnvironmentGroup := range applicationsLinkedEnvironmentGroups { if linkedEnvironmentGroup == environmentGroupName { apps = append(apps, LinkedPorterApplication{ Name: d.Name, Namespace: d.Namespace, }) } } } cronListResp, err := a.Clientset.BatchV1().CronJobs(metav1.NamespaceAll).List(ctx, metav1.ListOptions{ LabelSelector: LabelKey_LinkedEnvironmentGroup, }) if err != nil { return nil, telemetry.Error(ctx, span, err, "unable to list linked cronjob applications") } for _, d := range cronListResp.Items { applicationsLinkedEnvironmentGroups := strings.Split(d.Labels[LabelKey_LinkedEnvironmentGroup], ".") for _, linkedEnvironmentGroup := range applicationsLinkedEnvironmentGroups { if linkedEnvironmentGroup == environmentGroupName { apps = append(apps, LinkedPorterApplication{ Name: d.Name, Namespace: d.Namespace, }) } } } return apps, nil }