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 }