1 /* 2 Copyright 2024 The Flux authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package v2 18 19 import ( 20 "fmt" 21 "sort" 22 23 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 ) 25 26 const ( 27 // snapshotStatusDeployed indicates that the release the snapshot was taken 28 // from is currently deployed. 29 snapshotStatusDeployed = "deployed" 30 // snapshotStatusSuperseded indicates that the release the snapshot was taken 31 // from has been superseded by a newer release. 32 snapshotStatusSuperseded = "superseded" 33 34 // snapshotTestPhaseFailed indicates that the test of the release the snapshot 35 // was taken from has failed. 36 snapshotTestPhaseFailed = "Failed" 37 ) 38 39 // Snapshots is a list of Snapshot objects. 40 type Snapshots []*Snapshot 41 42 // Len returns the number of Snapshots. 43 func (in Snapshots) Len() int { 44 return len(in) 45 } 46 47 // SortByVersion sorts the Snapshots by version, in descending order. 48 func (in Snapshots) SortByVersion() { 49 sort.Slice(in, func(i, j int) bool { 50 return in[i].Version > in[j].Version 51 }) 52 } 53 54 // Latest returns the most recent Snapshot. 55 func (in Snapshots) Latest() *Snapshot { 56 if len(in) == 0 { 57 return nil 58 } 59 in.SortByVersion() 60 return in[0] 61 } 62 63 // Previous returns the most recent Snapshot before the Latest that has a 64 // status of "deployed" or "superseded", or nil if there is no such Snapshot. 65 // Unless ignoreTests is true, Snapshots with a test in the "Failed" phase are 66 // ignored. 67 func (in Snapshots) Previous(ignoreTests bool) *Snapshot { 68 if len(in) < 2 { 69 return nil 70 } 71 in.SortByVersion() 72 for i := range in[1:] { 73 s := in[i+1] 74 if s.Status == snapshotStatusDeployed || s.Status == snapshotStatusSuperseded { 75 if ignoreTests || !s.HasTestInPhase(snapshotTestPhaseFailed) { 76 return s 77 } 78 } 79 } 80 return nil 81 } 82 83 // Truncate removes all Snapshots up to the Previous deployed Snapshot. 84 // If there is no previous-deployed Snapshot, the most recent 5 Snapshots are 85 // retained. 86 func (in *Snapshots) Truncate(ignoreTests bool) { 87 if in.Len() < 2 { 88 return 89 } 90 91 in.SortByVersion() 92 for i := range (*in)[1:] { 93 s := (*in)[i+1] 94 if s.Status == snapshotStatusDeployed || s.Status == snapshotStatusSuperseded { 95 if ignoreTests || !s.HasTestInPhase(snapshotTestPhaseFailed) { 96 *in = (*in)[:i+2] 97 return 98 } 99 } 100 } 101 102 if in.Len() > defaultMaxHistory { 103 // If none of the Snapshots are deployed or superseded, and there 104 // are more than the defaultMaxHistory, truncate to the most recent 105 // Snapshots. 106 *in = (*in)[:defaultMaxHistory] 107 } 108 } 109 110 // Snapshot captures a point-in-time copy of the status information for a Helm release, 111 // as managed by the controller. 112 type Snapshot struct { 113 // APIVersion is the API version of the Snapshot. 114 // Provisional: when the calculation method of the Digest field is changed, 115 // this field will be used to distinguish between the old and new methods. 116 // +optional 117 APIVersion string `json:"apiVersion,omitempty"` 118 // Digest is the checksum of the release object in storage. 119 // It has the format of `<algo>:<checksum>`. 120 // +required 121 Digest string `json:"digest"` 122 // Name is the name of the release. 123 // +required 124 Name string `json:"name"` 125 // Namespace is the namespace the release is deployed to. 126 // +required 127 Namespace string `json:"namespace"` 128 // Version is the version of the release object in storage. 129 // +required 130 Version int `json:"version"` 131 // Status is the current state of the release. 132 // +required 133 Status string `json:"status"` 134 // ChartName is the chart name of the release object in storage. 135 // +required 136 ChartName string `json:"chartName"` 137 // ChartVersion is the chart version of the release object in 138 // storage. 139 // +required 140 ChartVersion string `json:"chartVersion"` 141 // AppVersion is the chart app version of the release object in storage. 142 // +optional 143 AppVersion string `json:"appVersion,omitempty"` 144 // ConfigDigest is the checksum of the config (better known as 145 // "values") of the release object in storage. 146 // It has the format of `<algo>:<checksum>`. 147 // +required 148 ConfigDigest string `json:"configDigest"` 149 // FirstDeployed is when the release was first deployed. 150 // +required 151 FirstDeployed metav1.Time `json:"firstDeployed"` 152 // LastDeployed is when the release was last deployed. 153 // +required 154 LastDeployed metav1.Time `json:"lastDeployed"` 155 // Deleted is when the release was deleted. 156 // +optional 157 Deleted metav1.Time `json:"deleted,omitempty"` 158 // TestHooks is the list of test hooks for the release as observed to be 159 // run by the controller. 160 // +optional 161 TestHooks *map[string]*TestHookStatus `json:"testHooks,omitempty"` 162 // OCIDigest is the digest of the OCI artifact associated with the release. 163 // +optional 164 OCIDigest string `json:"ociDigest,omitempty"` 165 } 166 167 // FullReleaseName returns the full name of the release in the format 168 // of '<namespace>/<name>.<version> 169 func (in *Snapshot) FullReleaseName() string { 170 if in == nil { 171 return "" 172 } 173 return fmt.Sprintf("%s/%s.v%d", in.Namespace, in.Name, in.Version) 174 } 175 176 // VersionedChartName returns the full name of the chart in the format of 177 // '<name>@<version>'. 178 func (in *Snapshot) VersionedChartName() string { 179 if in == nil { 180 return "" 181 } 182 return fmt.Sprintf("%s@%s", in.ChartName, in.ChartVersion) 183 } 184 185 // HasBeenTested returns true if TestHooks is not nil. This includes an empty 186 // map, which indicates the chart has no tests. 187 func (in *Snapshot) HasBeenTested() bool { 188 return in != nil && in.TestHooks != nil 189 } 190 191 // GetTestHooks returns the TestHooks for the release if not nil. 192 func (in *Snapshot) GetTestHooks() map[string]*TestHookStatus { 193 if in == nil || in.TestHooks == nil { 194 return nil 195 } 196 return *in.TestHooks 197 } 198 199 // HasTestInPhase returns true if any of the TestHooks is in the given phase. 200 func (in *Snapshot) HasTestInPhase(phase string) bool { 201 if in != nil { 202 for _, h := range in.GetTestHooks() { 203 if h.Phase == phase { 204 return true 205 } 206 } 207 } 208 return false 209 } 210 211 // SetTestHooks sets the TestHooks for the release. 212 func (in *Snapshot) SetTestHooks(hooks map[string]*TestHookStatus) { 213 if in == nil || hooks == nil { 214 return 215 } 216 in.TestHooks = &hooks 217 } 218 219 // Targets returns true if the Snapshot targets the given release data. 220 func (in *Snapshot) Targets(name, namespace string, version int) bool { 221 if in != nil { 222 return in.Name == name && in.Namespace == namespace && in.Version == version 223 } 224 return false 225 } 226 227 // TestHookStatus holds the status information for a test hook as observed 228 // to be run by the controller. 229 type TestHookStatus struct { 230 // LastStarted is the time the test hook was last started. 231 // +optional 232 LastStarted metav1.Time `json:"lastStarted,omitempty"` 233 // LastCompleted is the time the test hook last completed. 234 // +optional 235 LastCompleted metav1.Time `json:"lastCompleted,omitempty"` 236 // Phase the test hook was observed to be in. 237 // +optional 238 Phase string `json:"phase,omitempty"` 239 } 240