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  }