1 /* 2 Copyright 2023 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 v2beta2 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 // ConfigDigest is the checksum of the config (better known as 142 // "values") of the release object in storage. 143 // It has the format of `<algo>:<checksum>`. 144 // +required 145 ConfigDigest string `json:"configDigest"` 146 // FirstDeployed is when the release was first deployed. 147 // +required 148 FirstDeployed metav1.Time `json:"firstDeployed"` 149 // LastDeployed is when the release was last deployed. 150 // +required 151 LastDeployed metav1.Time `json:"lastDeployed"` 152 // Deleted is when the release was deleted. 153 // +optional 154 Deleted metav1.Time `json:"deleted,omitempty"` 155 // TestHooks is the list of test hooks for the release as observed to be 156 // run by the controller. 157 // +optional 158 TestHooks *map[string]*TestHookStatus `json:"testHooks,omitempty"` 159 // OCIDigest is the digest of the OCI artifact associated with the release. 160 // +optional 161 OCIDigest string `json:"ociDigest,omitempty"` 162 } 163 164 // FullReleaseName returns the full name of the release in the format 165 // of '<namespace>/<name>.<version> 166 func (in *Snapshot) FullReleaseName() string { 167 if in == nil { 168 return "" 169 } 170 return fmt.Sprintf("%s/%s.v%d", in.Namespace, in.Name, in.Version) 171 } 172 173 // VersionedChartName returns the full name of the chart in the format of 174 // '<name>@<version>'. 175 func (in *Snapshot) VersionedChartName() string { 176 if in == nil { 177 return "" 178 } 179 return fmt.Sprintf("%s@%s", in.ChartName, in.ChartVersion) 180 } 181 182 // HasBeenTested returns true if TestHooks is not nil. This includes an empty 183 // map, which indicates the chart has no tests. 184 func (in *Snapshot) HasBeenTested() bool { 185 return in != nil && in.TestHooks != nil 186 } 187 188 // GetTestHooks returns the TestHooks for the release if not nil. 189 func (in *Snapshot) GetTestHooks() map[string]*TestHookStatus { 190 if in == nil || in.TestHooks == nil { 191 return nil 192 } 193 return *in.TestHooks 194 } 195 196 // HasTestInPhase returns true if any of the TestHooks is in the given phase. 197 func (in *Snapshot) HasTestInPhase(phase string) bool { 198 if in != nil { 199 for _, h := range in.GetTestHooks() { 200 if h.Phase == phase { 201 return true 202 } 203 } 204 } 205 return false 206 } 207 208 // SetTestHooks sets the TestHooks for the release. 209 func (in *Snapshot) SetTestHooks(hooks map[string]*TestHookStatus) { 210 if in == nil || hooks == nil { 211 return 212 } 213 in.TestHooks = &hooks 214 } 215 216 // Targets returns true if the Snapshot targets the given release data. 217 func (in *Snapshot) Targets(name, namespace string, version int) bool { 218 if in != nil { 219 return in.Name == name && in.Namespace == namespace && in.Version == version 220 } 221 return false 222 } 223 224 // TestHookStatus holds the status information for a test hook as observed 225 // to be run by the controller. 226 type TestHookStatus struct { 227 // LastStarted is the time the test hook was last started. 228 // +optional 229 LastStarted metav1.Time `json:"lastStarted,omitempty"` 230 // LastCompleted is the time the test hook last completed. 231 // +optional 232 LastCompleted metav1.Time `json:"lastCompleted,omitempty"` 233 // Phase the test hook was observed to be in. 234 // +optional 235 Phase string `json:"phase,omitempty"` 236 } 237