github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/pkg/storage/driver/cfgmaps.go (about) 1 /* 2 Copyright The Helm 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 driver // import "github.com/stefanmcshane/helm/pkg/storage/driver" 18 19 import ( 20 "context" 21 "strconv" 22 "strings" 23 "time" 24 25 "github.com/pkg/errors" 26 v1 "k8s.io/api/core/v1" 27 apierrors "k8s.io/apimachinery/pkg/api/errors" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 kblabels "k8s.io/apimachinery/pkg/labels" 30 "k8s.io/apimachinery/pkg/util/validation" 31 corev1 "k8s.io/client-go/kubernetes/typed/core/v1" 32 33 rspb "github.com/stefanmcshane/helm/pkg/release" 34 ) 35 36 var _ Driver = (*ConfigMaps)(nil) 37 38 // ConfigMapsDriverName is the string name of the driver. 39 const ConfigMapsDriverName = "ConfigMap" 40 41 // ConfigMaps is a wrapper around an implementation of a kubernetes 42 // ConfigMapsInterface. 43 type ConfigMaps struct { 44 impl corev1.ConfigMapInterface 45 Log func(string, ...interface{}) 46 } 47 48 // NewConfigMaps initializes a new ConfigMaps wrapping an implementation of 49 // the kubernetes ConfigMapsInterface. 50 func NewConfigMaps(impl corev1.ConfigMapInterface) *ConfigMaps { 51 return &ConfigMaps{ 52 impl: impl, 53 Log: func(_ string, _ ...interface{}) {}, 54 } 55 } 56 57 // Name returns the name of the driver. 58 func (cfgmaps *ConfigMaps) Name() string { 59 return ConfigMapsDriverName 60 } 61 62 // Get fetches the release named by key. The corresponding release is returned 63 // or error if not found. 64 func (cfgmaps *ConfigMaps) Get(key string) (*rspb.Release, error) { 65 // fetch the configmap holding the release named by key 66 obj, err := cfgmaps.impl.Get(context.Background(), key, metav1.GetOptions{}) 67 if err != nil { 68 if apierrors.IsNotFound(err) { 69 return nil, ErrReleaseNotFound 70 } 71 72 cfgmaps.Log("get: failed to get %q: %s", key, err) 73 return nil, err 74 } 75 // found the configmap, decode the base64 data string 76 r, err := decodeRelease(obj.Data["release"]) 77 if err != nil { 78 cfgmaps.Log("get: failed to decode data %q: %s", key, err) 79 return nil, err 80 } 81 // return the release object 82 return r, nil 83 } 84 85 // List fetches all releases and returns the list releases such 86 // that filter(release) == true. An error is returned if the 87 // configmap fails to retrieve the releases. 88 func (cfgmaps *ConfigMaps) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { 89 lsel := kblabels.Set{"owner": "helm"}.AsSelector() 90 opts := metav1.ListOptions{LabelSelector: lsel.String()} 91 92 list, err := cfgmaps.impl.List(context.Background(), opts) 93 if err != nil { 94 cfgmaps.Log("list: failed to list: %s", err) 95 return nil, err 96 } 97 98 var results []*rspb.Release 99 100 // iterate over the configmaps object list 101 // and decode each release 102 for _, item := range list.Items { 103 rls, err := decodeRelease(item.Data["release"]) 104 if err != nil { 105 cfgmaps.Log("list: failed to decode release: %v: %s", item, err) 106 continue 107 } 108 109 rls.Labels = item.ObjectMeta.Labels 110 111 if filter(rls) { 112 results = append(results, rls) 113 } 114 } 115 return results, nil 116 } 117 118 // Query fetches all releases that match the provided map of labels. 119 // An error is returned if the configmap fails to retrieve the releases. 120 func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]*rspb.Release, error) { 121 ls := kblabels.Set{} 122 for k, v := range labels { 123 if errs := validation.IsValidLabelValue(v); len(errs) != 0 { 124 return nil, errors.Errorf("invalid label value: %q: %s", v, strings.Join(errs, "; ")) 125 } 126 ls[k] = v 127 } 128 129 opts := metav1.ListOptions{LabelSelector: ls.AsSelector().String()} 130 131 list, err := cfgmaps.impl.List(context.Background(), opts) 132 if err != nil { 133 cfgmaps.Log("query: failed to query with labels: %s", err) 134 return nil, err 135 } 136 137 if len(list.Items) == 0 { 138 return nil, ErrReleaseNotFound 139 } 140 141 var results []*rspb.Release 142 for _, item := range list.Items { 143 rls, err := decodeRelease(item.Data["release"]) 144 if err != nil { 145 cfgmaps.Log("query: failed to decode release: %s", err) 146 continue 147 } 148 results = append(results, rls) 149 } 150 return results, nil 151 } 152 153 // Create creates a new ConfigMap holding the release. If the 154 // ConfigMap already exists, ErrReleaseExists is returned. 155 func (cfgmaps *ConfigMaps) Create(key string, rls *rspb.Release) error { 156 // set labels for configmaps object meta data 157 var lbs labels 158 159 lbs.init() 160 lbs.set("createdAt", strconv.Itoa(int(time.Now().Unix()))) 161 162 // create a new configmap to hold the release 163 obj, err := newConfigMapsObject(key, rls, lbs) 164 if err != nil { 165 cfgmaps.Log("create: failed to encode release %q: %s", rls.Name, err) 166 return err 167 } 168 // push the configmap object out into the kubiverse 169 if _, err := cfgmaps.impl.Create(context.Background(), obj, metav1.CreateOptions{}); err != nil { 170 if apierrors.IsAlreadyExists(err) { 171 return ErrReleaseExists 172 } 173 174 cfgmaps.Log("create: failed to create: %s", err) 175 return err 176 } 177 return nil 178 } 179 180 // Update updates the ConfigMap holding the release. If not found 181 // the ConfigMap is created to hold the release. 182 func (cfgmaps *ConfigMaps) Update(key string, rls *rspb.Release) error { 183 // set labels for configmaps object meta data 184 var lbs labels 185 186 lbs.init() 187 lbs.set("modifiedAt", strconv.Itoa(int(time.Now().Unix()))) 188 189 // create a new configmap object to hold the release 190 obj, err := newConfigMapsObject(key, rls, lbs) 191 if err != nil { 192 cfgmaps.Log("update: failed to encode release %q: %s", rls.Name, err) 193 return err 194 } 195 // push the configmap object out into the kubiverse 196 _, err = cfgmaps.impl.Update(context.Background(), obj, metav1.UpdateOptions{}) 197 if err != nil { 198 cfgmaps.Log("update: failed to update: %s", err) 199 return err 200 } 201 return nil 202 } 203 204 // Delete deletes the ConfigMap holding the release named by key. 205 func (cfgmaps *ConfigMaps) Delete(key string) (rls *rspb.Release, err error) { 206 // fetch the release to check existence 207 if rls, err = cfgmaps.Get(key); err != nil { 208 return nil, err 209 } 210 // delete the release 211 if err = cfgmaps.impl.Delete(context.Background(), key, metav1.DeleteOptions{}); err != nil { 212 return rls, err 213 } 214 return rls, nil 215 } 216 217 // newConfigMapsObject constructs a kubernetes ConfigMap object 218 // to store a release. Each configmap data entry is the base64 219 // encoded gzipped string of a release. 220 // 221 // The following labels are used within each configmap: 222 // 223 // "modifiedAt" - timestamp indicating when this configmap was last modified. (set in Update) 224 // "createdAt" - timestamp indicating when this configmap was created. (set in Create) 225 // "version" - version of the release. 226 // "status" - status of the release (see pkg/release/status.go for variants) 227 // "owner" - owner of the configmap, currently "helm". 228 // "name" - name of the release. 229 // 230 func newConfigMapsObject(key string, rls *rspb.Release, lbs labels) (*v1.ConfigMap, error) { 231 const owner = "helm" 232 233 // encode the release 234 s, err := encodeRelease(rls) 235 if err != nil { 236 return nil, err 237 } 238 239 if lbs == nil { 240 lbs.init() 241 } 242 243 // apply labels 244 lbs.set("name", rls.Name) 245 lbs.set("owner", owner) 246 lbs.set("status", rls.Info.Status.String()) 247 lbs.set("version", strconv.Itoa(rls.Version)) 248 249 // create and return configmap object 250 return &v1.ConfigMap{ 251 ObjectMeta: metav1.ObjectMeta{ 252 Name: key, 253 Labels: lbs.toMap(), 254 }, 255 Data: map[string]string{"release": s}, 256 }, nil 257 }