github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/pkg/cluster/state.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // SPDX-FileCopyrightText: 2021-Present The Jackal Authors
     3  
     4  // Package cluster contains Jackal-specific cluster management functions.
     5  package cluster
     6  
     7  import (
     8  	"encoding/json"
     9  	"fmt"
    10  	"time"
    11  
    12  	"slices"
    13  
    14  	"github.com/Racer159/jackal/src/config"
    15  	"github.com/Racer159/jackal/src/config/lang"
    16  	"github.com/Racer159/jackal/src/types"
    17  	"github.com/fatih/color"
    18  
    19  	"github.com/Racer159/jackal/src/pkg/k8s"
    20  	"github.com/Racer159/jackal/src/pkg/message"
    21  	"github.com/Racer159/jackal/src/pkg/pki"
    22  	"github.com/defenseunicorns/pkg/helpers"
    23  	corev1 "k8s.io/api/core/v1"
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  )
    26  
    27  // Jackal Cluster Constants.
    28  const (
    29  	JackalNamespaceName       = "jackal"
    30  	JackalStateSecretName     = "jackal-state"
    31  	JackalStateDataKey        = "state"
    32  	JackalPackageInfoLabel    = "package-deploy-info"
    33  	JackalInitPackageInfoName = "jackal-package-init"
    34  )
    35  
    36  // InitJackalState initializes the Jackal state with the given temporary directory and init configs.
    37  func (c *Cluster) InitJackalState(initOptions types.JackalInitOptions) error {
    38  	var (
    39  		distro string
    40  		err    error
    41  	)
    42  
    43  	spinner := message.NewProgressSpinner("Gathering cluster state information")
    44  	defer spinner.Stop()
    45  
    46  	// Attempt to load an existing state prior to init.
    47  	// NOTE: We are ignoring the error here because we don't really expect a state to exist yet.
    48  	spinner.Updatef("Checking cluster for existing Jackal deployment")
    49  	state, _ := c.LoadJackalState()
    50  
    51  	// If state is nil, this is a new cluster.
    52  	if state == nil {
    53  		state = &types.JackalState{}
    54  		spinner.Updatef("New cluster, no prior Jackal deployments found")
    55  
    56  		// If the K3s component is being deployed, skip distro detection.
    57  		if initOptions.ApplianceMode {
    58  			distro = k8s.DistroIsK3s
    59  			state.JackalAppliance = true
    60  		} else {
    61  			// Otherwise, trying to detect the K8s distro type.
    62  			distro, err = c.DetectDistro()
    63  			if err != nil {
    64  				// This is a basic failure right now but likely could be polished to provide user guidance to resolve.
    65  				return fmt.Errorf("unable to connect to the cluster to verify the distro: %w", err)
    66  			}
    67  		}
    68  
    69  		if distro != k8s.DistroIsUnknown {
    70  			spinner.Updatef("Detected K8s distro %s", distro)
    71  		}
    72  
    73  		// Defaults
    74  		state.Distro = distro
    75  		if state.LoggingSecret, err = helpers.RandomString(types.JackalGeneratedPasswordLen); err != nil {
    76  			return fmt.Errorf("%s: %w", lang.ErrUnableToGenerateRandomSecret, err)
    77  		}
    78  
    79  		// Setup jackal agent PKI
    80  		state.AgentTLS = pki.GeneratePKI(config.JackalAgentHost)
    81  
    82  		namespaces, err := c.GetNamespaces()
    83  		if err != nil {
    84  			return fmt.Errorf("unable to get the Kubernetes namespaces: %w", err)
    85  		}
    86  		// Mark existing namespaces as ignored for the jackal agent to prevent mutating resources we don't own.
    87  		for _, namespace := range namespaces.Items {
    88  			spinner.Updatef("Marking existing namespace %s as ignored by Jackal Agent", namespace.Name)
    89  			if namespace.Labels == nil {
    90  				// Ensure label map exists to avoid nil panic
    91  				namespace.Labels = make(map[string]string)
    92  			}
    93  			// This label will tell the Jackal Agent to ignore this namespace.
    94  			namespace.Labels[agentLabel] = "ignore"
    95  			namespaceCopy := namespace
    96  			if _, err = c.UpdateNamespace(&namespaceCopy); err != nil {
    97  				// This is not a hard failure, but we should log it.
    98  				message.WarnErrf(err, "Unable to mark the namespace %s as ignored by Jackal Agent", namespace.Name)
    99  			}
   100  		}
   101  
   102  		// Try to create the jackal namespace.
   103  		spinner.Updatef("Creating the Jackal namespace")
   104  		jackalNamespace := c.NewJackalManagedNamespace(JackalNamespaceName)
   105  		if _, err := c.CreateNamespace(jackalNamespace); err != nil {
   106  			return fmt.Errorf("unable to create the jackal namespace: %w", err)
   107  		}
   108  
   109  		// Wait up to 2 minutes for the default service account to be created.
   110  		// Some clusters seem to take a while to create this, see https://github.com/kubernetes/kubernetes/issues/66689.
   111  		// The default SA is required for pods to start properly.
   112  		if _, err := c.WaitForServiceAccount(JackalNamespaceName, "default", 2*time.Minute); err != nil {
   113  			return fmt.Errorf("unable get default Jackal service account: %w", err)
   114  		}
   115  
   116  		err = initOptions.GitServer.FillInEmptyValues()
   117  		if err != nil {
   118  			return err
   119  		}
   120  		state.GitServer = initOptions.GitServer
   121  		err = initOptions.RegistryInfo.FillInEmptyValues()
   122  		if err != nil {
   123  			return err
   124  		}
   125  		state.RegistryInfo = initOptions.RegistryInfo
   126  		initOptions.ArtifactServer.FillInEmptyValues()
   127  		state.ArtifactServer = initOptions.ArtifactServer
   128  	} else {
   129  		if helpers.IsNotZeroAndNotEqual(initOptions.GitServer, state.GitServer) {
   130  			message.Warn("Detected a change in Git Server init options on a re-init. Ignoring... To update run:")
   131  			message.JackalCommand("tools update-creds git")
   132  		}
   133  		if helpers.IsNotZeroAndNotEqual(initOptions.RegistryInfo, state.RegistryInfo) {
   134  			message.Warn("Detected a change in Image Registry init options on a re-init. Ignoring... To update run:")
   135  			message.JackalCommand("tools update-creds registry")
   136  		}
   137  		if helpers.IsNotZeroAndNotEqual(initOptions.ArtifactServer, state.ArtifactServer) {
   138  			message.Warn("Detected a change in Artifact Server init options on a re-init. Ignoring... To update run:")
   139  			message.JackalCommand("tools update-creds artifact")
   140  		}
   141  	}
   142  
   143  	switch state.Distro {
   144  	case k8s.DistroIsK3s, k8s.DistroIsK3d:
   145  		state.StorageClass = "local-path"
   146  
   147  	case k8s.DistroIsKind, k8s.DistroIsGKE:
   148  		state.StorageClass = "standard"
   149  
   150  	case k8s.DistroIsDockerDesktop:
   151  		state.StorageClass = "hostpath"
   152  	}
   153  
   154  	if initOptions.StorageClass != "" {
   155  		state.StorageClass = initOptions.StorageClass
   156  	}
   157  
   158  	spinner.Success()
   159  
   160  	// Save the state back to K8s
   161  	if err := c.SaveJackalState(state); err != nil {
   162  		return fmt.Errorf("unable to save the Jackal state: %w", err)
   163  	}
   164  
   165  	return nil
   166  }
   167  
   168  // LoadJackalState returns the current jackal/jackal-state secret data or an empty JackalState.
   169  func (c *Cluster) LoadJackalState() (state *types.JackalState, err error) {
   170  	// Set up the API connection
   171  	secret, err := c.GetSecret(JackalNamespaceName, JackalStateSecretName)
   172  	if err != nil {
   173  		return nil, fmt.Errorf("%w. %s", err, message.ColorWrap("Did you remember to jackal init?", color.Bold))
   174  	}
   175  
   176  	err = json.Unmarshal(secret.Data[JackalStateDataKey], &state)
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  
   181  	c.debugPrintJackalState(state)
   182  
   183  	return state, nil
   184  }
   185  
   186  func (c *Cluster) sanitizeJackalState(state *types.JackalState) *types.JackalState {
   187  	// Overwrite the AgentTLS information
   188  	state.AgentTLS.CA = []byte("**sanitized**")
   189  	state.AgentTLS.Cert = []byte("**sanitized**")
   190  	state.AgentTLS.Key = []byte("**sanitized**")
   191  
   192  	// Overwrite the GitServer passwords
   193  	state.GitServer.PushPassword = "**sanitized**"
   194  	state.GitServer.PullPassword = "**sanitized**"
   195  
   196  	// Overwrite the RegistryInfo passwords
   197  	state.RegistryInfo.PushPassword = "**sanitized**"
   198  	state.RegistryInfo.PullPassword = "**sanitized**"
   199  	state.RegistryInfo.Secret = "**sanitized**"
   200  
   201  	// Overwrite the ArtifactServer secret
   202  	state.ArtifactServer.PushToken = "**sanitized**"
   203  
   204  	// Overwrite the Logging secret
   205  	state.LoggingSecret = "**sanitized**"
   206  
   207  	return state
   208  }
   209  
   210  func (c *Cluster) debugPrintJackalState(state *types.JackalState) {
   211  	if state == nil {
   212  		return
   213  	}
   214  	// this is a shallow copy, nested pointers WILL NOT be copied
   215  	oldState := *state
   216  	sanitized := c.sanitizeJackalState(&oldState)
   217  	message.Debugf("JackalState - %s", message.JSONValue(sanitized))
   218  }
   219  
   220  // SaveJackalState takes a given state and persists it to the Jackal/jackal-state secret.
   221  func (c *Cluster) SaveJackalState(state *types.JackalState) error {
   222  	c.debugPrintJackalState(state)
   223  
   224  	// Convert the data back to JSON.
   225  	data, err := json.Marshal(&state)
   226  	if err != nil {
   227  		return err
   228  	}
   229  
   230  	// Set up the data wrapper.
   231  	dataWrapper := make(map[string][]byte)
   232  	dataWrapper[JackalStateDataKey] = data
   233  
   234  	// The secret object.
   235  	secret := &corev1.Secret{
   236  		TypeMeta: metav1.TypeMeta{
   237  			APIVersion: corev1.SchemeGroupVersion.String(),
   238  			Kind:       "Secret",
   239  		},
   240  		ObjectMeta: metav1.ObjectMeta{
   241  			Name:      JackalStateSecretName,
   242  			Namespace: JackalNamespaceName,
   243  			Labels: map[string]string{
   244  				config.JackalManagedByLabel: "jackal",
   245  			},
   246  		},
   247  		Type: corev1.SecretTypeOpaque,
   248  		Data: dataWrapper,
   249  	}
   250  
   251  	// Attempt to create or update the secret and return.
   252  	if _, err := c.CreateOrUpdateSecret(secret); err != nil {
   253  		return fmt.Errorf("unable to create the jackal state secret")
   254  	}
   255  
   256  	return nil
   257  }
   258  
   259  // MergeJackalState merges init options for provided services into the provided state to create a new state struct
   260  func (c *Cluster) MergeJackalState(oldState *types.JackalState, initOptions types.JackalInitOptions, services []string) (*types.JackalState, error) {
   261  	newState := *oldState
   262  	var err error
   263  	if slices.Contains(services, message.RegistryKey) {
   264  		newState.RegistryInfo = helpers.MergeNonZero(newState.RegistryInfo, initOptions.RegistryInfo)
   265  		// Set the state of the internal registry if it has changed
   266  		if newState.RegistryInfo.Address == fmt.Sprintf("%s:%d", helpers.IPV4Localhost, newState.RegistryInfo.NodePort) {
   267  			newState.RegistryInfo.InternalRegistry = true
   268  		} else {
   269  			newState.RegistryInfo.InternalRegistry = false
   270  		}
   271  
   272  		// Set the new passwords if they should be autogenerated
   273  		if newState.RegistryInfo.PushPassword == oldState.RegistryInfo.PushPassword && oldState.RegistryInfo.InternalRegistry {
   274  			if newState.RegistryInfo.PushPassword, err = helpers.RandomString(types.JackalGeneratedPasswordLen); err != nil {
   275  				return nil, fmt.Errorf("%s: %w", lang.ErrUnableToGenerateRandomSecret, err)
   276  			}
   277  		}
   278  		if newState.RegistryInfo.PullPassword == oldState.RegistryInfo.PullPassword && oldState.RegistryInfo.InternalRegistry {
   279  			if newState.RegistryInfo.PullPassword, err = helpers.RandomString(types.JackalGeneratedPasswordLen); err != nil {
   280  				return nil, fmt.Errorf("%s: %w", lang.ErrUnableToGenerateRandomSecret, err)
   281  			}
   282  		}
   283  	}
   284  	if slices.Contains(services, message.GitKey) {
   285  		newState.GitServer = helpers.MergeNonZero(newState.GitServer, initOptions.GitServer)
   286  
   287  		// Set the state of the internal git server if it has changed
   288  		if newState.GitServer.Address == types.JackalInClusterGitServiceURL {
   289  			newState.GitServer.InternalServer = true
   290  		} else {
   291  			newState.GitServer.InternalServer = false
   292  		}
   293  
   294  		// Set the new passwords if they should be autogenerated
   295  		if newState.GitServer.PushPassword == oldState.GitServer.PushPassword && oldState.GitServer.InternalServer {
   296  			if newState.GitServer.PushPassword, err = helpers.RandomString(types.JackalGeneratedPasswordLen); err != nil {
   297  				return nil, fmt.Errorf("%s: %w", lang.ErrUnableToGenerateRandomSecret, err)
   298  			}
   299  		}
   300  		if newState.GitServer.PullPassword == oldState.GitServer.PullPassword && oldState.GitServer.InternalServer {
   301  			if newState.GitServer.PullPassword, err = helpers.RandomString(types.JackalGeneratedPasswordLen); err != nil {
   302  				return nil, fmt.Errorf("%s: %w", lang.ErrUnableToGenerateRandomSecret, err)
   303  			}
   304  		}
   305  	}
   306  	if slices.Contains(services, message.ArtifactKey) {
   307  		newState.ArtifactServer = helpers.MergeNonZero(newState.ArtifactServer, initOptions.ArtifactServer)
   308  
   309  		// Set the state of the internal artifact server if it has changed
   310  		if newState.ArtifactServer.Address == types.JackalInClusterArtifactServiceURL {
   311  			newState.ArtifactServer.InternalServer = true
   312  		} else {
   313  			newState.ArtifactServer.InternalServer = false
   314  		}
   315  
   316  		// Set an empty token if it should be autogenerated
   317  		if newState.ArtifactServer.PushToken == oldState.ArtifactServer.PushToken && oldState.ArtifactServer.InternalServer {
   318  			newState.ArtifactServer.PushToken = ""
   319  		}
   320  	}
   321  	if slices.Contains(services, message.AgentKey) {
   322  		newState.AgentTLS = pki.GeneratePKI(config.JackalAgentHost)
   323  	}
   324  
   325  	return &newState, nil
   326  }