<commit_before>/*
   Copyright The containerd Authors.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package metadata

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/containerd/containerd/errdefs"
	"github.com/containerd/containerd/filters"
	"github.com/containerd/containerd/leases"
	"github.com/containerd/containerd/metadata/boltutil"
	"github.com/containerd/containerd/namespaces"
	digest "github.com/opencontainers/go-digest"
	"github.com/pkg/errors"
	bolt "go.etcd.io/bbolt"
)

// LeaseManager manages the create/delete lifecyle of leases
// and also returns existing leases
type LeaseManager struct {
	tx *bolt.Tx
}

// NewLeaseManager creates a new lease manager for managing leases using
// the provided database transaction.
func NewLeaseManager(tx *bolt.Tx) *LeaseManager {
	return &LeaseManager{
		tx: tx,
	}
}

// Create creates a new lease using the provided lease
func (lm *LeaseManager) Create(ctx context.Context, opts ...leases.Opt) (leases.Lease, error) {
	var l leases.Lease
	for _, opt := range opts {
		if err := opt(&l); err != nil {
			return leases.Lease{}, err
		}
	}
	if l.ID == "" {
		return leases.Lease{}, errors.New("lease id must be provided")
	}

	namespace, err := namespaces.NamespaceRequired(ctx)
	if err != nil {
		return leases.Lease{}, err
	}

	topbkt, err := createBucketIfNotExists(lm.tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases)
	if err != nil {
		return leases.Lease{}, err
	}

	txbkt, err := topbkt.CreateBucket([]byte(l.ID))
	if err != nil {
		if err == bolt.ErrBucketExists {
			err = errdefs.ErrAlreadyExists
		}
		return leases.Lease{}, errors.Wrapf(err, "lease %q", l.ID)
	}

	t := time.Now().UTC()
	createdAt, err := t.MarshalBinary()
	if err != nil {
		return leases.Lease{}, err
	}
	if err := txbkt.Put(bucketKeyCreatedAt, createdAt); err != nil {
		return leases.Lease{}, err
	}

	if l.Labels != nil {
		if err := boltutil.WriteLabels(txbkt, l.Labels); err != nil {
			return leases.Lease{}, err
		}
	}
	l.CreatedAt = t

	return l, nil
}

// Delete delets the lease with the provided lease ID
func (lm *LeaseManager) Delete(ctx context.Context, lease leases.Lease, _ ...leases.DeleteOpt) error {
	namespace, err := namespaces.NamespaceRequired(ctx)
	if err != nil {
		return err
	}

	topbkt := getBucket(lm.tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases)
	if topbkt == nil {
		return errors.Wrapf(errdefs.ErrNotFound, "lease %q", lease.ID)
	}
	if err := topbkt.DeleteBucket([]byte(lease.ID)); err != nil {
		if err == bolt.ErrBucketNotFound {
			err = errors.Wrapf(errdefs.ErrNotFound, "lease %q", lease.ID)
		}
		return err
	}
	return nil
}

// List lists all active leases
func (lm *LeaseManager) List(ctx context.Context, fs ...string) ([]leases.Lease, error) {
	namespace, err := namespaces.NamespaceRequired(ctx)
	if err != nil {
		return nil, err
	}

	filter, err := filters.ParseAll(fs...)
	if err != nil {
		return nil, errors.Wrap(errdefs.ErrInvalidArgument, err.Error())
	}

	var ll []leases.Lease

	topbkt := getBucket(lm.tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases)
	if topbkt == nil {
		return ll, nil
	}

	if err := topbkt.ForEach(func(k, v []byte) error {
		if v != nil {
			return nil
		}
		txbkt := topbkt.Bucket(k)

		l := leases.Lease{
			ID: string(k),
		}

		if v := txbkt.Get(bucketKeyCreatedAt); v != nil {
			t := &l.CreatedAt
			if err := t.UnmarshalBinary(v); err != nil {
				return err
			}
		}

		labels, err := boltutil.ReadLabels(txbkt)
		if err != nil {
			return err
		}
		l.Labels = labels

		if filter.Match(adaptLease(l)) {
			ll = append(ll, l)
		}

		return nil
	}); err != nil {
		return nil, err
	}

	return ll, nil
}

// AddResource references the resource by the provided lease.
func (lm *LeaseManager) AddResource(ctx context.Context, lease leases.Lease, r leases.Resource) error {
	namespace, err := namespaces.NamespaceRequired(ctx)
	if err != nil {
		return err
	}

	topbkt := getBucket(lm.tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases, []byte(lease.ID))
	if topbkt == nil {
		return errors.Wrapf(errdefs.ErrNotFound, "lease %q", lease.ID)
	}

	keys, ref, err := parseLeaseResource(r)
	if err != nil {
		return err
	}

	bkt := topbkt
	for _, key := range keys {
		bkt, err = bkt.CreateBucketIfNotExists([]byte(key))
		if err != nil {
			return err
		}
	}
	return bkt.Put([]byte(ref), nil)
}

// DeleteResource dereferences the resource by the provided lease.
func (lm *LeaseManager) DeleteResource(ctx context.Context, lease leases.Lease, r leases.Resource) error {
	namespace, err := namespaces.NamespaceRequired(ctx)
	if err != nil {
		return err
	}

	topbkt := getBucket(lm.tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases, []byte(lease.ID))
	if topbkt == nil {
		return errors.Wrapf(errdefs.ErrNotFound, "lease %q", lease.ID)
	}

	keys, ref, err := parseLeaseResource(r)
	if err != nil {
		return err
	}

	bkt := topbkt
	for _, key := range keys {
		if bkt == nil {
			break
		}
		bkt = bkt.Bucket([]byte(key))
	}

	if bkt == nil {
		return nil
	}
	return bkt.Delete([]byte(ref))
}

// ListResources lists all the resources referenced by the lease.
func (lm *LeaseManager) ListResources(ctx context.Context, lease leases.Lease) ([]leases.Resource, error) {
	namespace, err := namespaces.NamespaceRequired(ctx)
	if err != nil {
		return nil, err
	}

	topbkt := getBucket(lm.tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases, []byte(lease.ID))
	if topbkt == nil {
		return nil, errors.Wrapf(errdefs.ErrNotFound, "lease %q", lease.ID)
	}

	rs := make([]leases.Resource, 0)

	// content resources
	if cbkt := topbkt.Bucket(bucketKeyObjectContent); cbkt != nil {
		if err := cbkt.ForEach(func(k, _ []byte) error {
			rs = append(rs, leases.Resource{
				ID:   string(k),
				Type: string(bucketKeyObjectContent),
			})

			return nil
		}); err != nil {
			return nil, err
		}
	}

	// ingest resources
	if lbkt := topbkt.Bucket(bucketKeyObjectIngests); lbkt != nil {
		if err := lbkt.ForEach(func(k, _ []byte) error {
			rs = append(rs, leases.Resource{
				ID:   string(k),
				Type: string(bucketKeyObjectIngests),
			})

			return nil
		}); err != nil {
			return nil, err
		}
	}

	// snapshot resources
	if sbkt := topbkt.Bucket(bucketKeyObjectSnapshots); sbkt != nil {
		if err := sbkt.ForEach(func(sk, sv []byte) error {
			if sv != nil {
				return nil
			}

			snbkt := sbkt.Bucket(sk)
			return snbkt.ForEach(func(k, _ []byte) error {
				rs = append(rs, leases.Resource{
					ID:   string(k),
					Type: fmt.Sprintf("%s/%s", bucketKeyObjectSnapshots, sk),
				})
				return nil
			})
		}); err != nil {
			return nil, err
		}
	}
	return rs, nil
}

func addSnapshotLease(ctx context.Context, tx *bolt.Tx, snapshotter, key string) error {
	lid, ok := leases.FromContext(ctx)
	if !ok {
		return nil
	}

	namespace, ok := namespaces.Namespace(ctx)
	if !ok {
		panic("namespace must already be checked")
	}

	bkt := getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases, []byte(lid))
	if bkt == nil {
		return errors.Wrap(errdefs.ErrNotFound, "lease does not exist")
	}

	bkt, err := bkt.CreateBucketIfNotExists(bucketKeyObjectSnapshots)
	if err != nil {
		return err
	}

	bkt, err = bkt.CreateBucketIfNotExists([]byte(snapshotter))
	if err != nil {
		return err
	}

	return bkt.Put([]byte(key), nil)
}

func removeSnapshotLease(ctx context.Context, tx *bolt.Tx, snapshotter, key string) error {
	lid, ok := leases.FromContext(ctx)
	if !ok {
		return nil
	}

	namespace, ok := namespaces.Namespace(ctx)
	if !ok {
		panic("namespace must already be checked")
	}

	bkt := getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases, []byte(lid), bucketKeyObjectSnapshots, []byte(snapshotter))
	if bkt == nil {
		// Key does not exist so we return nil
		return nil
	}

	return bkt.Delete([]byte(key))
}

func addContentLease(ctx context.Context, tx *bolt.Tx, dgst digest.Digest) error {
	lid, ok := leases.FromContext(ctx)
	if !ok {
		return nil
	}

	namespace, ok := namespaces.Namespace(ctx)
	if !ok {
		panic("namespace must already be required")
	}

	bkt := getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases, []byte(lid))
	if bkt == nil {
		return errors.Wrap(errdefs.ErrNotFound, "lease does not exist")
	}

	bkt, err := bkt.CreateBucketIfNotExists(bucketKeyObjectContent)
	if err != nil {
		return err
	}

	return bkt.Put([]byte(dgst.String()), nil)
}

func removeContentLease(ctx context.Context, tx *bolt.Tx, dgst digest.Digest) error {
	lid, ok := leases.FromContext(ctx)
	if !ok {
		return nil
	}

	namespace, ok := namespaces.Namespace(ctx)
	if !ok {
		panic("namespace must already be checked")
	}

	bkt := getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases, []byte(lid), bucketKeyObjectContent)
	if bkt == nil {
		// Key does not exist so we return nil
		return nil
	}

	return bkt.Delete([]byte(dgst.String()))
}

func addIngestLease(ctx context.Context, tx *bolt.Tx, ref string) (bool, error) {
	lid, ok := leases.FromContext(ctx)
	if !ok {
		return false, nil
	}

	namespace, ok := namespaces.Namespace(ctx)
	if !ok {
		panic("namespace must already be required")
	}

	bkt := getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases, []byte(lid))
	if bkt == nil {
		return false, errors.Wrap(errdefs.ErrNotFound, "lease does not exist")
	}

	bkt, err := bkt.CreateBucketIfNotExists(bucketKeyObjectIngests)
	if err != nil {
		return false, err
	}

	if err := bkt.Put([]byte(ref), nil); err != nil {
		return false, err
	}

	return true, nil
}

func removeIngestLease(ctx context.Context, tx *bolt.Tx, ref string) error {
	lid, ok := leases.FromContext(ctx)
	if !ok {
		return nil
	}

	namespace, ok := namespaces.Namespace(ctx)
	if !ok {
		panic("namespace must already be checked")
	}

	bkt := getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases, []byte(lid), bucketKeyObjectIngests)
	if bkt == nil {
		// Key does not exist so we return nil
		return nil
	}

	return bkt.Delete([]byte(ref))
}

func parseLeaseResource(r leases.Resource) ([]string, string, error) {
	var (
		ref  = r.ID
		typ  = r.Type
		keys = strings.Split(typ, "/")
	)

	switch k := keys[0]; k {
	case string(bucketKeyObjectContent),
		string(bucketKeyObjectIngests):

		if len(keys) != 1 {
			return nil, "", errors.Wrapf(errdefs.ErrInvalidArgument, "invalid resource type %s", typ)
		}

		if k == string(bucketKeyObjectContent) {
			dgst, err := digest.Parse(ref)
			if err != nil {
				return nil, "", errors.Wrapf(errdefs.ErrInvalidArgument, "invalid content resource id %s: %v", ref, err)
			}
			ref = dgst.String()
		}
	case string(bucketKeyObjectSnapshots):
		if len(keys) != 2 {
			return nil, "", errors.Wrapf(errdefs.ErrInvalidArgument, "invalid snapshot resource type %s", typ)
		}
	default:
		return nil, "", errors.Wrapf(errdefs.ErrNotImplemented, "resource type %s not supported yet", typ)
	}

	return keys, ref, nil
}
<commit_msg>cleanup code typos in leases.go<commit_after>/*
   Copyright The containerd Authors.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package metadata

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/containerd/containerd/errdefs"
	"github.com/containerd/containerd/filters"
	"github.com/containerd/containerd/leases"
	"github.com/containerd/containerd/metadata/boltutil"
	"github.com/containerd/containerd/namespaces"
	digest "github.com/opencontainers/go-digest"
	"github.com/pkg/errors"
	bolt "go.etcd.io/bbolt"
)

// LeaseManager manages the create/delete lifecycle of leases
// and also returns existing leases
type LeaseManager struct {
	tx *bolt.Tx
}

// NewLeaseManager creates a new lease manager for managing leases using
// the provided database transaction.
func NewLeaseManager(tx *bolt.Tx) *LeaseManager {
	return &LeaseManager{
		tx: tx,
	}
}

// Create creates a new lease using the provided lease
func (lm *LeaseManager) Create(ctx context.Context, opts ...leases.Opt) (leases.Lease, error) {
	var l leases.Lease
	for _, opt := range opts {
		if err := opt(&l); err != nil {
			return leases.Lease{}, err
		}
	}
	if l.ID == "" {
		return leases.Lease{}, errors.New("lease id must be provided")
	}

	namespace, err := namespaces.NamespaceRequired(ctx)
	if err != nil {
		return leases.Lease{}, err
	}

	topbkt, err := createBucketIfNotExists(lm.tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases)
	if err != nil {
		return leases.Lease{}, err
	}

	txbkt, err := topbkt.CreateBucket([]byte(l.ID))
	if err != nil {
		if err == bolt.ErrBucketExists {
			err = errdefs.ErrAlreadyExists
		}
		return leases.Lease{}, errors.Wrapf(err, "lease %q", l.ID)
	}

	t := time.Now().UTC()
	createdAt, err := t.MarshalBinary()
	if err != nil {
		return leases.Lease{}, err
	}
	if err := txbkt.Put(bucketKeyCreatedAt, createdAt); err != nil {
		return leases.Lease{}, err
	}

	if l.Labels != nil {
		if err := boltutil.WriteLabels(txbkt, l.Labels); err != nil {
			return leases.Lease{}, err
		}
	}
	l.CreatedAt = t

	return l, nil
}

// Delete deletes the lease with the provided lease ID
func (lm *LeaseManager) Delete(ctx context.Context, lease leases.Lease, _ ...leases.DeleteOpt) error {
	namespace, err := namespaces.NamespaceRequired(ctx)
	if err != nil {
		return err
	}

	topbkt := getBucket(lm.tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases)
	if topbkt == nil {
		return errors.Wrapf(errdefs.ErrNotFound, "lease %q", lease.ID)
	}
	if err := topbkt.DeleteBucket([]byte(lease.ID)); err != nil {
		if err == bolt.ErrBucketNotFound {
			err = errors.Wrapf(errdefs.ErrNotFound, "lease %q", lease.ID)
		}
		return err
	}
	return nil
}

// List lists all active leases
func (lm *LeaseManager) List(ctx context.Context, fs ...string) ([]leases.Lease, error) {
	namespace, err := namespaces.NamespaceRequired(ctx)
	if err != nil {
		return nil, err
	}

	filter, err := filters.ParseAll(fs...)
	if err != nil {
		return nil, errors.Wrap(errdefs.ErrInvalidArgument, err.Error())
	}

	var ll []leases.Lease

	topbkt := getBucket(lm.tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases)
	if topbkt == nil {
		return ll, nil
	}

	if err := topbkt.ForEach(func(k, v []byte) error {
		if v != nil {
			return nil
		}
		txbkt := topbkt.Bucket(k)

		l := leases.Lease{
			ID: string(k),
		}

		if v := txbkt.Get(bucketKeyCreatedAt); v != nil {
			t := &l.CreatedAt
			if err := t.UnmarshalBinary(v); err != nil {
				return err
			}
		}

		labels, err := boltutil.ReadLabels(txbkt)
		if err != nil {
			return err
		}
		l.Labels = labels

		if filter.Match(adaptLease(l)) {
			ll = append(ll, l)
		}

		return nil
	}); err != nil {
		return nil, err
	}

	return ll, nil
}

// AddResource references the resource by the provided lease.
func (lm *LeaseManager) AddResource(ctx context.Context, lease leases.Lease, r leases.Resource) error {
	namespace, err := namespaces.NamespaceRequired(ctx)
	if err != nil {
		return err
	}

	topbkt := getBucket(lm.tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases, []byte(lease.ID))
	if topbkt == nil {
		return errors.Wrapf(errdefs.ErrNotFound, "lease %q", lease.ID)
	}

	keys, ref, err := parseLeaseResource(r)
	if err != nil {
		return err
	}

	bkt := topbkt
	for _, key := range keys {
		bkt, err = bkt.CreateBucketIfNotExists([]byte(key))
		if err != nil {
			return err
		}
	}
	return bkt.Put([]byte(ref), nil)
}

// DeleteResource dereferences the resource by the provided lease.
func (lm *LeaseManager) DeleteResource(ctx context.Context, lease leases.Lease, r leases.Resource) error {
	namespace, err := namespaces.NamespaceRequired(ctx)
	if err != nil {
		return err
	}

	topbkt := getBucket(lm.tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases, []byte(lease.ID))
	if topbkt == nil {
		return errors.Wrapf(errdefs.ErrNotFound, "lease %q", lease.ID)
	}

	keys, ref, err := parseLeaseResource(r)
	if err != nil {
		return err
	}

	bkt := topbkt
	for _, key := range keys {
		if bkt == nil {
			break
		}
		bkt = bkt.Bucket([]byte(key))
	}

	if bkt == nil {
		return nil
	}
	return bkt.Delete([]byte(ref))
}

// ListResources lists all the resources referenced by the lease.
func (lm *LeaseManager) ListResources(ctx context.Context, lease leases.Lease) ([]leases.Resource, error) {
	namespace, err := namespaces.NamespaceRequired(ctx)
	if err != nil {
		return nil, err
	}

	topbkt := getBucket(lm.tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases, []byte(lease.ID))
	if topbkt == nil {
		return nil, errors.Wrapf(errdefs.ErrNotFound, "lease %q", lease.ID)
	}

	rs := make([]leases.Resource, 0)

	// content resources
	if cbkt := topbkt.Bucket(bucketKeyObjectContent); cbkt != nil {
		if err := cbkt.ForEach(func(k, _ []byte) error {
			rs = append(rs, leases.Resource{
				ID:   string(k),
				Type: string(bucketKeyObjectContent),
			})

			return nil
		}); err != nil {
			return nil, err
		}
	}

	// ingest resources
	if lbkt := topbkt.Bucket(bucketKeyObjectIngests); lbkt != nil {
		if err := lbkt.ForEach(func(k, _ []byte) error {
			rs = append(rs, leases.Resource{
				ID:   string(k),
				Type: string(bucketKeyObjectIngests),
			})

			return nil
		}); err != nil {
			return nil, err
		}
	}

	// snapshot resources
	if sbkt := topbkt.Bucket(bucketKeyObjectSnapshots); sbkt != nil {
		if err := sbkt.ForEach(func(sk, sv []byte) error {
			if sv != nil {
				return nil
			}

			snbkt := sbkt.Bucket(sk)
			return snbkt.ForEach(func(k, _ []byte) error {
				rs = append(rs, leases.Resource{
					ID:   string(k),
					Type: fmt.Sprintf("%s/%s", bucketKeyObjectSnapshots, sk),
				})
				return nil
			})
		}); err != nil {
			return nil, err
		}
	}
	return rs, nil
}

func addSnapshotLease(ctx context.Context, tx *bolt.Tx, snapshotter, key string) error {
	lid, ok := leases.FromContext(ctx)
	if !ok {
		return nil
	}

	namespace, ok := namespaces.Namespace(ctx)
	if !ok {
		panic("namespace must already be checked")
	}

	bkt := getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases, []byte(lid))
	if bkt == nil {
		return errors.Wrap(errdefs.ErrNotFound, "lease does not exist")
	}

	bkt, err := bkt.CreateBucketIfNotExists(bucketKeyObjectSnapshots)
	if err != nil {
		return err
	}

	bkt, err = bkt.CreateBucketIfNotExists([]byte(snapshotter))
	if err != nil {
		return err
	}

	return bkt.Put([]byte(key), nil)
}

func removeSnapshotLease(ctx context.Context, tx *bolt.Tx, snapshotter, key string) error {
	lid, ok := leases.FromContext(ctx)
	if !ok {
		return nil
	}

	namespace, ok := namespaces.Namespace(ctx)
	if !ok {
		panic("namespace must already be checked")
	}

	bkt := getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases, []byte(lid), bucketKeyObjectSnapshots, []byte(snapshotter))
	if bkt == nil {
		// Key does not exist so we return nil
		return nil
	}

	return bkt.Delete([]byte(key))
}

func addContentLease(ctx context.Context, tx *bolt.Tx, dgst digest.Digest) error {
	lid, ok := leases.FromContext(ctx)
	if !ok {
		return nil
	}

	namespace, ok := namespaces.Namespace(ctx)
	if !ok {
		panic("namespace must already be required")
	}

	bkt := getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases, []byte(lid))
	if bkt == nil {
		return errors.Wrap(errdefs.ErrNotFound, "lease does not exist")
	}

	bkt, err := bkt.CreateBucketIfNotExists(bucketKeyObjectContent)
	if err != nil {
		return err
	}

	return bkt.Put([]byte(dgst.String()), nil)
}

func removeContentLease(ctx context.Context, tx *bolt.Tx, dgst digest.Digest) error {
	lid, ok := leases.FromContext(ctx)
	if !ok {
		return nil
	}

	namespace, ok := namespaces.Namespace(ctx)
	if !ok {
		panic("namespace must already be checked")
	}

	bkt := getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases, []byte(lid), bucketKeyObjectContent)
	if bkt == nil {
		// Key does not exist so we return nil
		return nil
	}

	return bkt.Delete([]byte(dgst.String()))
}

func addIngestLease(ctx context.Context, tx *bolt.Tx, ref string) (bool, error) {
	lid, ok := leases.FromContext(ctx)
	if !ok {
		return false, nil
	}

	namespace, ok := namespaces.Namespace(ctx)
	if !ok {
		panic("namespace must already be required")
	}

	bkt := getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases, []byte(lid))
	if bkt == nil {
		return false, errors.Wrap(errdefs.ErrNotFound, "lease does not exist")
	}

	bkt, err := bkt.CreateBucketIfNotExists(bucketKeyObjectIngests)
	if err != nil {
		return false, err
	}

	if err := bkt.Put([]byte(ref), nil); err != nil {
		return false, err
	}

	return true, nil
}

func removeIngestLease(ctx context.Context, tx *bolt.Tx, ref string) error {
	lid, ok := leases.FromContext(ctx)
	if !ok {
		return nil
	}

	namespace, ok := namespaces.Namespace(ctx)
	if !ok {
		panic("namespace must already be checked")
	}

	bkt := getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectLeases, []byte(lid), bucketKeyObjectIngests)
	if bkt == nil {
		// Key does not exist so we return nil
		return nil
	}

	return bkt.Delete([]byte(ref))
}

func parseLeaseResource(r leases.Resource) ([]string, string, error) {
	var (
		ref  = r.ID
		typ  = r.Type
		keys = strings.Split(typ, "/")
	)

	switch k := keys[0]; k {
	case string(bucketKeyObjectContent),
		string(bucketKeyObjectIngests):

		if len(keys) != 1 {
			return nil, "", errors.Wrapf(errdefs.ErrInvalidArgument, "invalid resource type %s", typ)
		}

		if k == string(bucketKeyObjectContent) {
			dgst, err := digest.Parse(ref)
			if err != nil {
				return nil, "", errors.Wrapf(errdefs.ErrInvalidArgument, "invalid content resource id %s: %v", ref, err)
			}
			ref = dgst.String()
		}
	case string(bucketKeyObjectSnapshots):
		if len(keys) != 2 {
			return nil, "", errors.Wrapf(errdefs.ErrInvalidArgument, "invalid snapshot resource type %s", typ)
		}
	default:
		return nil, "", errors.Wrapf(errdefs.ErrNotImplemented, "resource type %s not supported yet", typ)
	}

	return keys, ref, nil
}
<|endoftext|>


<commit_before>package test

// Utility functions for more complex go tests
// Need to be in a separate test package so they can be imported anywhere
// Also can't add _test.go suffix to exclude from main build (import doesn't work)

// To avoid import cycles, append "_test" to the package statement of any test using
// this package and use "import . original/package/name" to get the same visibility
// as if the test was in the same package (as usual)

import (
	"fmt"
	"io"
	"io/ioutil"
	"math/rand"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/github/git-lfs/git"
	"github.com/github/git-lfs/lfs"
)

type TestRepoType int

const (
	// Normal repo with working copy
	TestRepoTypeNormal = TestRepoType(iota)
	// Bare repo (no working copy)
	TestRepoTypeBare = TestRepoType(iota)
	// Repo with working copy but git dir is separate
	TestRepoTypeSeparateDir = TestRepoType(iota)
)

type TestRepoCreateSettings struct {
	RepoType TestRepoType
}

type TestRepo struct {
	// Path to the repo, working copy if non-bare
	Path string
	// Path to the git dir
	GitDir string
	// Settings used to create this repo
	Settings *TestRepoCreateSettings
}

func (r *TestRepo) Cleanup() {
	if r.GitDir != "" {
		os.RemoveAll(r.GitDir)
	}
	if r.Path != "" {
		os.RemoveAll(r.Path)
	}
}

func NewTestRepo(t *testing.T) *TestRepo {
	return NewCustomTestRepo(t, &TestRepoCreateSettings{RepoType: TestRepoTypeNormal})
}
func NewCustomTestRepo(t *testing.T, settings *TestRepoCreateSettings) *TestRepo {
	ret := &TestRepo{Settings: settings}

	path, err := ioutil.TempDir("", "lfstestrepo")
	if err != nil {
		t.Fatalf("Can't create temp dir for git repo: %v", err)
	}
	ret.Path = path
	args := []string{"init"}
	switch settings.RepoType {
	case TestRepoTypeBare:
		args = append(args, "--bare")
		ret.GitDir = ret.Path
	case TestRepoTypeSeparateDir:
		gitdir, err := ioutil.TempDir("", "lfstestgitdir")
		if err != nil {
			ret.Cleanup()
			t.Fatalf("Can't create temp dir for git repo: %v", err)
		}
		args = append(args, "--separate-dir", gitdir)
		ret.GitDir = gitdir
	default:
		ret.GitDir = filepath.Join(ret.Path, ".git")
	}
	args = append(args, path)
	cmd := exec.Command("git", args...)
	err = cmd.Run()
	if err != nil {
		ret.Cleanup()
		t.Fatalf("Unable to create git repo at %v: %v", path, err)
	}
	return ret
}

// Simplistic fire & forget running of git command - returns combined output
func RunGitCommand(t *testing.T, failureCheck bool, args ...string) string {
	outp, err := exec.Command("git", args...).CombinedOutput()
	if failureCheck && err != nil {
		t.Fatalf("Error running git command 'git %v': %v", strings.Join(args, " "), err)
	}
	return string(outp)

}

// Input data for a single file in a commit
type TestFileInput struct {
	// Name of file (required)
	Filename string
	// Size of file (required)
	Size int64
	// Input data (optional - if nil, placeholder data of Size will be created)
	Data io.Reader
}

// Input for defining commits for test repo
type TestCommitSetupInput struct {
	// Date that we should commit on (optional, leave blank for 'now')
	CommitDate time.Time
	// List of files to include in this commit
	Files []*TestFileInput
	// List of parent branches (all branches must have been created in a previous)
	// Can be omitted to just use the parent of the previous commit
	ParentBranches []string
	// Name of a new branch we should create at this commit (optional - master not required)
	NewBranch string
	// Name of committer
	CommitterName string
	// Email of committer
	CommitterEmail string
}

// Output struct with details of commits created for test
type TestCommitSetupOutput struct {
	Sha     string
	Parents []string
	Files   []*lfs.Pointer
}

func FormatGitDate(tm time.Time) string {
	// Git format is "Fri Jun 21 20:26:41 2013 +0900" but no zero-leading for day
	return tm.Format("Mon Jan 2 15:04:05 2006 -0700")
}

func CommitAtDate(atDate time.Time, committerName, committerEmail, msg string) error {
	var args []string
	if committerName != "" && committerEmail != "" {
		args = append(args, "-c", fmt.Sprintf("user.name=%v", committerName))
		args = append(args, "-c", fmt.Sprintf("user.email=%v", committerEmail))
	}
	args = append(args, "commit", "--allow-empty", "-m", msg)
	cmd := exec.Command("git", args...)
	env := os.Environ()
	// set GIT_COMMITTER_DATE environment var e.g. "Fri Jun 21 20:26:41 2013 +0900"
	if atDate.IsZero() {
		env = append(env, "GIT_COMMITTER_DATE=")
	} else {
		env = append(env, fmt.Sprintf("GIT_COMMITTER_DATE=%v", FormatGitDate(atDate)))
	}
	cmd.Env = env
	return cmd.Run()
}

func SetupRepo(t *testing.T, inputs []*TestCommitSetupInput) []*TestCommitSetupOutput {
	// Used to check whether we need to checkout another commit before
	lastBranch := "master"
	outputs := make([]*TestCommitSetupOutput, 0, len(inputs))

	for i, input := range inputs {
		output := &TestCommitSetupOutput{}
		// first, are we on the correct branch
		if len(input.ParentBranches) > 0 {
			if input.ParentBranches[0] != lastBranch {
				RunGitCommand(t, true, "checkout", input.ParentBranches[0])
				lastBranch = input.ParentBranches[0]
			}
		}
		// Is this a merge?
		if len(input.ParentBranches) > 1 {
			// Always take the *other* side in a merge so we adopt changes
			// also don't automatically commit, we'll do that below
			args := []string{"merge", "--no-ff", "--no-commit", "--strategy-option=theirs"}
			args = append(args, input.ParentBranches[1:]...)
			RunGitCommand(t, false, args...)
		} else if input.NewBranch != "" {
			RunGitCommand(t, true, "checkout", "-b", input.NewBranch)
			lastBranch = input.NewBranch
		}
		// Any files to write?
		for fi, infile := range input.Files {
			inputData := infile.Data
			if inputData == nil {
				// Different data for each file but deterministic
				inputData = NewPlaceholderDataReader(int64(i * fi))
			}
			cleaned, err := lfs.PointerClean(inputData, infile.Filename, infile.Size, nil)
			if err != nil {
				t.Errorf("Error creating pointer file: %v", err)
				continue
			}

			output.Files = append(output.Files, cleaned.Pointer)
			// Write pointer to local filename for adding (not using clean filter)
			os.MkdirAll(filepath.Dir(infile.Filename), 0755)
			f, err := os.Create(infile.Filename)
			if err != nil {
				t.Errorf("Error creating pointer file: %v", err)
				continue
			}
			_, err = cleaned.Pointer.Encode(f)
			if err != nil {
				f.Close()
				t.Errorf("Error encoding pointer file: %v", err)
				continue
			}
			f.Close() // early close in a loop, don't defer
			RunGitCommand(t, true, "add", infile.Filename)

		}
		// Now commit
		CommitAtDate(input.CommitDate, input.CommitterName, input.CommitterEmail,
			fmt.Sprintf("Test commit %d", i))
		commit, err := git.GetCommitSummary("HEAD")
		if err != nil {
			t.Fatalf("Error determining commit SHA: %v", err)
		}
		output.Sha = commit.Sha
		output.Parents = commit.Parents
		outputs = append(outputs, output)
	}
	return outputs
}

// Just a psuedo-random stream of bytes (not cryptographic)
// Calls RNG a bit less often than using rand.Source directly
type PlaceholderDataReader struct {
	source rand.Source
}

func NewPlaceholderDataReader(seed int64) *PlaceholderDataReader {
	return &PlaceholderDataReader{rand.NewSource(seed)}
}

func (r *PlaceholderDataReader) Read(p []byte) (int, error) {
	c := len(p)
	for i := 0; i < c; {
		// Use all 8 bytes of the 64-bit random number
		val64 := r.source.Int63()
		for j := 0; j < 8 && i < c; j++ {
			// Duplicate this byte 16 times (faster)
			for k := 0; k < 16; k++ {
				p[i] = byte(val64)
				i++
			}
			// Next byte from the 8-byte number
			val64 = val64 >> 8
		}
	}
	return len(p), nil
}
<commit_msg>ララララー ラララー ララ<commit_after>package test

// Utility functions for more complex go tests
// Need to be in a separate test package so they can be imported anywhere
// Also can't add _test.go suffix to exclude from main build (import doesn't work)

// To avoid import cycles, append "_test" to the package statement of any test using
// this package and use "import . original/package/name" to get the same visibility
// as if the test was in the same package (as usual)

import (
	"fmt"
	"io"
	"io/ioutil"
	"math/rand"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/github/git-lfs/git"
	"github.com/github/git-lfs/lfs"
)

type TestRepoType int

const (
	// Normal repo with working copy
	TestRepoTypeNormal = TestRepoType(iota)
	// Bare repo (no working copy)
	TestRepoTypeBare = TestRepoType(iota)
	// Repo with working copy but git dir is separate
	TestRepoTypeSeparateDir = TestRepoType(iota)
)

type TestRepoCreateSettings struct {
	RepoType TestRepoType
}

type TestRepo struct {
	// Path to the repo, working copy if non-bare
	Path string
	// Path to the git dir
	GitDir string
	// Settings used to create this repo
	Settings *TestRepoCreateSettings
}

func (r *TestRepo) Cleanup() {
	if r.GitDir != "" {
		os.RemoveAll(r.GitDir)
	}
	if r.Path != "" {
		os.RemoveAll(r.Path)
	}
}

func NewTestRepo(t *testing.T) *TestRepo {
	return NewCustomTestRepo(t, &TestRepoCreateSettings{RepoType: TestRepoTypeNormal})
}
func NewCustomTestRepo(t *testing.T, settings *TestRepoCreateSettings) *TestRepo {
	ret := &TestRepo{Settings: settings}

	path, err := ioutil.TempDir("", "lfstestrepo")
	if err != nil {
		t.Fatalf("Can't create temp dir for git repo: %v", err)
	}
	ret.Path = path
	args := []string{"init"}
	switch settings.RepoType {
	case TestRepoTypeBare:
		args = append(args, "--bare")
		ret.GitDir = ret.Path
	case TestRepoTypeSeparateDir:
		gitdir, err := ioutil.TempDir("", "lfstestgitdir")
		if err != nil {
			ret.Cleanup()
			t.Fatalf("Can't create temp dir for git repo: %v", err)
		}
		args = append(args, "--separate-dir", gitdir)
		ret.GitDir = gitdir
	default:
		ret.GitDir = filepath.Join(ret.Path, ".git")
	}
	args = append(args, path)
	cmd := exec.Command("git", args...)
	err = cmd.Run()
	if err != nil {
		ret.Cleanup()
		t.Fatalf("Unable to create git repo at %v: %v", path, err)
	}
	return ret
}

// Simplistic fire & forget running of git command - returns combined output
func RunGitCommand(t *testing.T, failureCheck bool, args ...string) string {
	outp, err := exec.Command("git", args...).CombinedOutput()
	if failureCheck && err != nil {
		t.Fatalf("Error running git command 'git %v': %v", strings.Join(args, " "), err)
	}
	return string(outp)

}

// Input data for a single file in a commit
type TestFileInput struct {
	// Name of file (required)
	Filename string
	// Size of file (required)
	Size int64
	// Input data (optional - if nil, placeholder data of Size will be created)
	Data io.Reader
}

// Input for defining commits for test repo
type TestCommitSetupInput struct {
	// Date that we should commit on (optional, leave blank for 'now')
	CommitDate time.Time
	// List of files to include in this commit
	Files []*TestFileInput
	// List of parent branches (all branches must have been created in a previous)
	// Can be omitted to just use the parent of the previous commit
	ParentBranches []string
	// Name of a new branch we should create at this commit (optional - master not required)
	NewBranch string
	// Name of committer
	CommitterName string
	// Email of committer
	CommitterEmail string
}

// Output struct with details of commits created for test
type TestCommitSetupOutput struct {
	Sha     string
	Parents []string
	Files   []*lfs.Pointer
}

func formatGitDate(tm time.Time) string {
	// Git format is "Fri Jun 21 20:26:41 2013 +0900" but no zero-leading for day
	return tm.Format("Mon Jan 2 15:04:05 2006 -0700")
}

func commitAtDate(atDate time.Time, committerName, committerEmail, msg string) error {
	var args []string
	if committerName != "" && committerEmail != "" {
		args = append(args, "-c", fmt.Sprintf("user.name=%v", committerName))
		args = append(args, "-c", fmt.Sprintf("user.email=%v", committerEmail))
	}
	args = append(args, "commit", "--allow-empty", "-m", msg)
	cmd := exec.Command("git", args...)
	env := os.Environ()
	// set GIT_COMMITTER_DATE environment var e.g. "Fri Jun 21 20:26:41 2013 +0900"
	if atDate.IsZero() {
		env = append(env, "GIT_COMMITTER_DATE=")
	} else {
		env = append(env, fmt.Sprintf("GIT_COMMITTER_DATE=%v", formatGitDate(atDate)))
	}
	cmd.Env = env
	return cmd.Run()
}

func (repo *TestRepo) AddCommits(t *testing.T, inputs []*TestCommitSetupInput) []*TestCommitSetupOutput {
	if repo.Settings.RepoType == TestRepoTypeBare {
		t.Fatalf("Cannot use SetupRepo on a bare repo; clone it & push changes instead")
	}

	// Change to repo working dir
	oldwd, err := os.Getwd()
	if err != nil {
		t.Fatalf("Can't get cwd %v", err)
	}
	err = os.Chdir(repo.Path)
	if err != nil {
		t.Fatalf("Can't chdir to repo %v", err)
	}
	// Used to check whether we need to checkout another commit before
	lastBranch := "master"
	outputs := make([]*TestCommitSetupOutput, 0, len(inputs))

	for i, input := range inputs {
		output := &TestCommitSetupOutput{}
		// first, are we on the correct branch
		if len(input.ParentBranches) > 0 {
			if input.ParentBranches[0] != lastBranch {
				RunGitCommand(t, true, "checkout", input.ParentBranches[0])
				lastBranch = input.ParentBranches[0]
			}
		}
		// Is this a merge?
		if len(input.ParentBranches) > 1 {
			// Always take the *other* side in a merge so we adopt changes
			// also don't automatically commit, we'll do that below
			args := []string{"merge", "--no-ff", "--no-commit", "--strategy-option=theirs"}
			args = append(args, input.ParentBranches[1:]...)
			RunGitCommand(t, false, args...)
		} else if input.NewBranch != "" {
			RunGitCommand(t, true, "checkout", "-b", input.NewBranch)
			lastBranch = input.NewBranch
		}
		// Any files to write?
		for fi, infile := range input.Files {
			inputData := infile.Data
			if inputData == nil {
				// Different data for each file but deterministic
				inputData = NewPlaceholderDataReader(int64(i * fi))
			}
			cleaned, err := lfs.PointerClean(inputData, infile.Filename, infile.Size, nil)
			if err != nil {
				t.Errorf("Error creating pointer file: %v", err)
				continue
			}

			output.Files = append(output.Files, cleaned.Pointer)
			// Write pointer to local filename for adding (not using clean filter)
			os.MkdirAll(filepath.Dir(infile.Filename), 0755)
			f, err := os.Create(infile.Filename)
			if err != nil {
				t.Errorf("Error creating pointer file: %v", err)
				continue
			}
			_, err = cleaned.Pointer.Encode(f)
			if err != nil {
				f.Close()
				t.Errorf("Error encoding pointer file: %v", err)
				continue
			}
			f.Close() // early close in a loop, don't defer
			RunGitCommand(t, true, "add", infile.Filename)

		}
		// Now commit
		commitAtDate(input.CommitDate, input.CommitterName, input.CommitterEmail,
			fmt.Sprintf("Test commit %d", i))
		commit, err := git.GetCommitSummary("HEAD")
		if err != nil {
			t.Fatalf("Error determining commit SHA: %v", err)
		}
		output.Sha = commit.Sha
		output.Parents = commit.Parents
		outputs = append(outputs, output)
	}

	// Restore cwd
	err = os.Chdir(oldwd)
	if err != nil {
		t.Fatalf("Can't restore old cwd %v", err)
	}

	return outputs
}

// Just a psuedo-random stream of bytes (not cryptographic)
// Calls RNG a bit less often than using rand.Source directly
type PlaceholderDataReader struct {
	source rand.Source
}

func NewPlaceholderDataReader(seed int64) *PlaceholderDataReader {
	return &PlaceholderDataReader{rand.NewSource(seed)}
}

func (r *PlaceholderDataReader) Read(p []byte) (int, error) {
	c := len(p)
	for i := 0; i < c; {
		// Use all 8 bytes of the 64-bit random number
		val64 := r.source.Int63()
		for j := 0; j < 8 && i < c; j++ {
			// Duplicate this byte 16 times (faster)
			for k := 0; k < 16; k++ {
				p[i] = byte(val64)
				i++
			}
			// Next byte from the 8-byte number
			val64 = val64 >> 8
		}
	}
	return len(p), nil
}
<|endoftext|>


<commit_before>/*
Copyright 2016 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package kuberuntime

import (
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"time"

	"github.com/golang/glog"
	"k8s.io/apimachinery/pkg/types"
	internalapi "k8s.io/kubernetes/pkg/kubelet/apis/cri"
	runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
	kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
)

// containerGC is the manager of garbage collection.
type containerGC struct {
	client              internalapi.RuntimeService
	manager             *kubeGenericRuntimeManager
	podDeletionProvider podDeletionProvider
}

// NewContainerGC creates a new containerGC.
func NewContainerGC(client internalapi.RuntimeService, podDeletionProvider podDeletionProvider, manager *kubeGenericRuntimeManager) *containerGC {
	return &containerGC{
		client:              client,
		manager:             manager,
		podDeletionProvider: podDeletionProvider,
	}
}

// containerGCInfo is the internal information kept for containers being considered for GC.
type containerGCInfo struct {
	// The ID of the container.
	id string
	// The name of the container.
	name string
	// Creation time for the container.
	createTime time.Time
}

// sandboxGCInfo is the internal information kept for sandboxes being considered for GC.
type sandboxGCInfo struct {
	// The ID of the sandbox.
	id string
	// Creation time for the sandbox.
	createTime time.Time
	// If true, the sandbox is ready or still has containers.
	active bool
}

// evictUnit is considered for eviction as units of (UID, container name) pair.
type evictUnit struct {
	// UID of the pod.
	uid types.UID
	// Name of the container in the pod.
	name string
}

type containersByEvictUnit map[evictUnit][]containerGCInfo
type sandboxesByPodUID map[types.UID][]sandboxGCInfo

// NumContainers returns the number of containers in this map.
func (cu containersByEvictUnit) NumContainers() int {
	num := 0
	for key := range cu {
		num += len(cu[key])
	}
	return num
}

// NumEvictUnits returns the number of pod in this map.
func (cu containersByEvictUnit) NumEvictUnits() int {
	return len(cu)
}

// Newest first.
type byCreated []containerGCInfo

func (a byCreated) Len() int           { return len(a) }
func (a byCreated) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a byCreated) Less(i, j int) bool { return a[i].createTime.After(a[j].createTime) }

// Newest first.
type sandboxByCreated []sandboxGCInfo

func (a sandboxByCreated) Len() int           { return len(a) }
func (a sandboxByCreated) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a sandboxByCreated) Less(i, j int) bool { return a[i].createTime.After(a[j].createTime) }

// enforceMaxContainersPerEvictUnit enforces MaxPerPodContainer for each evictUnit.
func (cgc *containerGC) enforceMaxContainersPerEvictUnit(evictUnits containersByEvictUnit, MaxContainers int) {
	for key := range evictUnits {
		toRemove := len(evictUnits[key]) - MaxContainers

		if toRemove > 0 {
			evictUnits[key] = cgc.removeOldestN(evictUnits[key], toRemove)
		}
	}
}

// removeOldestN removes the oldest toRemove containers and returns the resulting slice.
func (cgc *containerGC) removeOldestN(containers []containerGCInfo, toRemove int) []containerGCInfo {
	// Remove from oldest to newest (last to first).
	numToKeep := len(containers) - toRemove
	for i := len(containers) - 1; i >= numToKeep; i-- {
		if err := cgc.manager.removeContainer(containers[i].id); err != nil {
			glog.Errorf("Failed to remove container %q: %v", containers[i].id, err)
		}
	}

	// Assume we removed the containers so that we're not too aggressive.
	return containers[:numToKeep]
}

// removeOldestNSandboxes removes the oldest inactive toRemove sandboxes and
// returns the resulting slice.
func (cgc *containerGC) removeOldestNSandboxes(sandboxes []sandboxGCInfo, toRemove int) {
	// Remove from oldest to newest (last to first).
	numToKeep := len(sandboxes) - toRemove
	for i := len(sandboxes) - 1; i >= numToKeep; i-- {
		if !sandboxes[i].active {
			cgc.removeSandbox(sandboxes[i].id)
		}
	}
}

// removeSandbox removes the sandbox by sandboxID.
func (cgc *containerGC) removeSandbox(sandboxID string) {
	glog.V(4).Infof("Removing sandbox %q", sandboxID)
	// In normal cases, kubelet should've already called StopPodSandbox before
	// GC kicks in. To guard against the rare cases where this is not true, try
	// stopping the sandbox before removing it.
	if err := cgc.client.StopPodSandbox(sandboxID); err != nil {
		glog.Errorf("Failed to stop sandbox %q before removing: %v", sandboxID, err)
		return
	}
	if err := cgc.client.RemovePodSandbox(sandboxID); err != nil {
		glog.Errorf("Failed to remove sandbox %q: %v", sandboxID, err)
	}
}

// evictableContainers gets all containers that are evictable. Evictable containers are: not running
// and created more than MinAge ago.
func (cgc *containerGC) evictableContainers(minAge time.Duration) (containersByEvictUnit, error) {
	containers, err := cgc.manager.getKubeletContainers(true)
	if err != nil {
		return containersByEvictUnit{}, err
	}

	evictUnits := make(containersByEvictUnit)
	newestGCTime := time.Now().Add(-minAge)
	for _, container := range containers {
		// Prune out running containers.
		if container.State == runtimeapi.ContainerState_CONTAINER_RUNNING {
			continue
		}

		createdAt := time.Unix(0, container.CreatedAt)
		if newestGCTime.Before(createdAt) {
			continue
		}

		labeledInfo := getContainerInfoFromLabels(container.Labels)
		containerInfo := containerGCInfo{
			id:         container.Id,
			name:       container.Metadata.Name,
			createTime: createdAt,
		}
		key := evictUnit{
			uid:  labeledInfo.PodUID,
			name: containerInfo.name,
		}
		evictUnits[key] = append(evictUnits[key], containerInfo)
	}

	// Sort the containers by age.
	for uid := range evictUnits {
		sort.Sort(byCreated(evictUnits[uid]))
	}

	return evictUnits, nil
}

// evict all containers that are evictable
func (cgc *containerGC) evictContainers(gcPolicy kubecontainer.ContainerGCPolicy, allSourcesReady bool, evictNonDeletedPods bool) error {
	// Separate containers by evict units.
	evictUnits, err := cgc.evictableContainers(gcPolicy.MinAge)
	if err != nil {
		return err
	}

	// Remove deleted pod containers if all sources are ready.
	if allSourcesReady {
		for key, unit := range evictUnits {
			if cgc.podDeletionProvider.IsPodDeleted(key.uid) || evictNonDeletedPods {
				cgc.removeOldestN(unit, len(unit)) // Remove all.
				delete(evictUnits, key)
			}
		}
	}

	// Enforce max containers per evict unit.
	if gcPolicy.MaxPerPodContainer >= 0 {
		cgc.enforceMaxContainersPerEvictUnit(evictUnits, gcPolicy.MaxPerPodContainer)
	}

	// Enforce max total number of containers.
	if gcPolicy.MaxContainers >= 0 && evictUnits.NumContainers() > gcPolicy.MaxContainers {
		// Leave an equal number of containers per evict unit (min: 1).
		numContainersPerEvictUnit := gcPolicy.MaxContainers / evictUnits.NumEvictUnits()
		if numContainersPerEvictUnit < 1 {
			numContainersPerEvictUnit = 1
		}
		cgc.enforceMaxContainersPerEvictUnit(evictUnits, numContainersPerEvictUnit)

		// If we still need to evict, evict oldest first.
		numContainers := evictUnits.NumContainers()
		if numContainers > gcPolicy.MaxContainers {
			flattened := make([]containerGCInfo, 0, numContainers)
			for key := range evictUnits {
				flattened = append(flattened, evictUnits[key]...)
			}
			sort.Sort(byCreated(flattened))

			cgc.removeOldestN(flattened, numContainers-gcPolicy.MaxContainers)
		}
	}
	return nil
}

// evictSandboxes remove all evictable sandboxes. An evictable sandbox must
// meet the following requirements:
//   1. not in ready state
//   2. contains no containers.
//   3. belong to a non-existent (i.e., already removed) pod, or is not the
//      most recently created sandbox for the pod.
func (cgc *containerGC) evictSandboxes(evictNonDeletedPods bool) error {
	containers, err := cgc.manager.getKubeletContainers(true)
	if err != nil {
		return err
	}

	sandboxes, err := cgc.manager.getKubeletSandboxes(true)
	if err != nil {
		return err
	}

	sandboxesByPod := make(sandboxesByPodUID)
	for _, sandbox := range sandboxes {
		podUID := types.UID(sandbox.Metadata.Uid)
		sandboxInfo := sandboxGCInfo{
			id:         sandbox.Id,
			createTime: time.Unix(0, sandbox.CreatedAt),
		}

		// Set ready sandboxes to be active.
		if sandbox.State == runtimeapi.PodSandboxState_SANDBOX_READY {
			sandboxInfo.active = true
		}

		// Set sandboxes that still have containers to be active.
		hasContainers := false
		sandboxID := sandbox.Id
		for _, container := range containers {
			if container.PodSandboxId == sandboxID {
				hasContainers = true
				break
			}
		}
		if hasContainers {
			sandboxInfo.active = true
		}

		sandboxesByPod[podUID] = append(sandboxesByPod[podUID], sandboxInfo)
	}

	// Sort the sandboxes by age.
	for uid := range sandboxesByPod {
		sort.Sort(sandboxByCreated(sandboxesByPod[uid]))
	}

	for podUID, sandboxes := range sandboxesByPod {
		if cgc.podDeletionProvider.IsPodDeleted(podUID) || evictNonDeletedPods {
			// Remove all evictable sandboxes if the pod has been removed.
			// Note that the latest dead sandbox is also removed if there is
			// already an active one.
			cgc.removeOldestNSandboxes(sandboxes, len(sandboxes))
		} else {
			// Keep latest one if the pod still exists.
			cgc.removeOldestNSandboxes(sandboxes, len(sandboxes)-1)
		}
	}
	return nil
}

// evictPodLogsDirectories evicts all evictable pod logs directories. Pod logs directories
// are evictable if there are no corresponding pods.
func (cgc *containerGC) evictPodLogsDirectories(allSourcesReady bool) error {
	osInterface := cgc.manager.osInterface
	if allSourcesReady {
		// Only remove pod logs directories when all sources are ready.
		dirs, err := osInterface.ReadDir(podLogsRootDirectory)
		if err != nil {
			return fmt.Errorf("failed to read podLogsRootDirectory %q: %v", podLogsRootDirectory, err)
		}
		for _, dir := range dirs {
			name := dir.Name()
			podUID := types.UID(name)
			if !cgc.podDeletionProvider.IsPodDeleted(podUID) {
				continue
			}
			err := osInterface.RemoveAll(filepath.Join(podLogsRootDirectory, name))
			if err != nil {
				glog.Errorf("Failed to remove pod logs directory %q: %v", name, err)
			}
		}
	}

	// Remove dead container log symlinks.
	// TODO(random-liu): Remove this after cluster logging supports CRI container log path.
	logSymlinks, _ := osInterface.Glob(filepath.Join(legacyContainerLogsDir, fmt.Sprintf("*.%s", legacyLogSuffix)))
	for _, logSymlink := range logSymlinks {
		if _, err := osInterface.Stat(logSymlink); os.IsNotExist(err) {
			err := osInterface.Remove(logSymlink)
			if err != nil {
				glog.Errorf("Failed to remove container log dead symlink %q: %v", logSymlink, err)
			}
		}
	}
	return nil
}

// GarbageCollect removes dead containers using the specified container gc policy.
// Note that gc policy is not applied to sandboxes. Sandboxes are only removed when they are
// not ready and containing no containers.
//
// GarbageCollect consists of the following steps:
// * gets evictable containers which are not active and created more than gcPolicy.MinAge ago.
// * removes oldest dead containers for each pod by enforcing gcPolicy.MaxPerPodContainer.
// * removes oldest dead containers by enforcing gcPolicy.MaxContainers.
// * gets evictable sandboxes which are not ready and contains no containers.
// * removes evictable sandboxes.
func (cgc *containerGC) GarbageCollect(gcPolicy kubecontainer.ContainerGCPolicy, allSourcesReady bool, evictNonDeletedPods bool) error {
	// Remove evictable containers
	if err := cgc.evictContainers(gcPolicy, allSourcesReady, evictNonDeletedPods); err != nil {
		return err
	}

	// Remove sandboxes with zero containers
	if err := cgc.evictSandboxes(evictNonDeletedPods); err != nil {
		return err
	}

	// Remove pod sandbox log directory
	return cgc.evictPodLogsDirectories(allSourcesReady)
}
<commit_msg>Improve codes which checks whether sandbox contains containers<commit_after>/*
Copyright 2016 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package kuberuntime

import (
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"time"

	"github.com/golang/glog"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/sets"
	internalapi "k8s.io/kubernetes/pkg/kubelet/apis/cri"
	runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
	kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
)

// containerGC is the manager of garbage collection.
type containerGC struct {
	client              internalapi.RuntimeService
	manager             *kubeGenericRuntimeManager
	podDeletionProvider podDeletionProvider
}

// NewContainerGC creates a new containerGC.
func NewContainerGC(client internalapi.RuntimeService, podDeletionProvider podDeletionProvider, manager *kubeGenericRuntimeManager) *containerGC {
	return &containerGC{
		client:              client,
		manager:             manager,
		podDeletionProvider: podDeletionProvider,
	}
}

// containerGCInfo is the internal information kept for containers being considered for GC.
type containerGCInfo struct {
	// The ID of the container.
	id string
	// The name of the container.
	name string
	// Creation time for the container.
	createTime time.Time
}

// sandboxGCInfo is the internal information kept for sandboxes being considered for GC.
type sandboxGCInfo struct {
	// The ID of the sandbox.
	id string
	// Creation time for the sandbox.
	createTime time.Time
	// If true, the sandbox is ready or still has containers.
	active bool
}

// evictUnit is considered for eviction as units of (UID, container name) pair.
type evictUnit struct {
	// UID of the pod.
	uid types.UID
	// Name of the container in the pod.
	name string
}

type containersByEvictUnit map[evictUnit][]containerGCInfo
type sandboxesByPodUID map[types.UID][]sandboxGCInfo

// NumContainers returns the number of containers in this map.
func (cu containersByEvictUnit) NumContainers() int {
	num := 0
	for key := range cu {
		num += len(cu[key])
	}
	return num
}

// NumEvictUnits returns the number of pod in this map.
func (cu containersByEvictUnit) NumEvictUnits() int {
	return len(cu)
}

// Newest first.
type byCreated []containerGCInfo

func (a byCreated) Len() int           { return len(a) }
func (a byCreated) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a byCreated) Less(i, j int) bool { return a[i].createTime.After(a[j].createTime) }

// Newest first.
type sandboxByCreated []sandboxGCInfo

func (a sandboxByCreated) Len() int           { return len(a) }
func (a sandboxByCreated) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a sandboxByCreated) Less(i, j int) bool { return a[i].createTime.After(a[j].createTime) }

// enforceMaxContainersPerEvictUnit enforces MaxPerPodContainer for each evictUnit.
func (cgc *containerGC) enforceMaxContainersPerEvictUnit(evictUnits containersByEvictUnit, MaxContainers int) {
	for key := range evictUnits {
		toRemove := len(evictUnits[key]) - MaxContainers

		if toRemove > 0 {
			evictUnits[key] = cgc.removeOldestN(evictUnits[key], toRemove)
		}
	}
}

// removeOldestN removes the oldest toRemove containers and returns the resulting slice.
func (cgc *containerGC) removeOldestN(containers []containerGCInfo, toRemove int) []containerGCInfo {
	// Remove from oldest to newest (last to first).
	numToKeep := len(containers) - toRemove
	for i := len(containers) - 1; i >= numToKeep; i-- {
		if err := cgc.manager.removeContainer(containers[i].id); err != nil {
			glog.Errorf("Failed to remove container %q: %v", containers[i].id, err)
		}
	}

	// Assume we removed the containers so that we're not too aggressive.
	return containers[:numToKeep]
}

// removeOldestNSandboxes removes the oldest inactive toRemove sandboxes and
// returns the resulting slice.
func (cgc *containerGC) removeOldestNSandboxes(sandboxes []sandboxGCInfo, toRemove int) {
	// Remove from oldest to newest (last to first).
	numToKeep := len(sandboxes) - toRemove
	for i := len(sandboxes) - 1; i >= numToKeep; i-- {
		if !sandboxes[i].active {
			cgc.removeSandbox(sandboxes[i].id)
		}
	}
}

// removeSandbox removes the sandbox by sandboxID.
func (cgc *containerGC) removeSandbox(sandboxID string) {
	glog.V(4).Infof("Removing sandbox %q", sandboxID)
	// In normal cases, kubelet should've already called StopPodSandbox before
	// GC kicks in. To guard against the rare cases where this is not true, try
	// stopping the sandbox before removing it.
	if err := cgc.client.StopPodSandbox(sandboxID); err != nil {
		glog.Errorf("Failed to stop sandbox %q before removing: %v", sandboxID, err)
		return
	}
	if err := cgc.client.RemovePodSandbox(sandboxID); err != nil {
		glog.Errorf("Failed to remove sandbox %q: %v", sandboxID, err)
	}
}

// evictableContainers gets all containers that are evictable. Evictable containers are: not running
// and created more than MinAge ago.
func (cgc *containerGC) evictableContainers(minAge time.Duration) (containersByEvictUnit, error) {
	containers, err := cgc.manager.getKubeletContainers(true)
	if err != nil {
		return containersByEvictUnit{}, err
	}

	evictUnits := make(containersByEvictUnit)
	newestGCTime := time.Now().Add(-minAge)
	for _, container := range containers {
		// Prune out running containers.
		if container.State == runtimeapi.ContainerState_CONTAINER_RUNNING {
			continue
		}

		createdAt := time.Unix(0, container.CreatedAt)
		if newestGCTime.Before(createdAt) {
			continue
		}

		labeledInfo := getContainerInfoFromLabels(container.Labels)
		containerInfo := containerGCInfo{
			id:         container.Id,
			name:       container.Metadata.Name,
			createTime: createdAt,
		}
		key := evictUnit{
			uid:  labeledInfo.PodUID,
			name: containerInfo.name,
		}
		evictUnits[key] = append(evictUnits[key], containerInfo)
	}

	// Sort the containers by age.
	for uid := range evictUnits {
		sort.Sort(byCreated(evictUnits[uid]))
	}

	return evictUnits, nil
}

// evict all containers that are evictable
func (cgc *containerGC) evictContainers(gcPolicy kubecontainer.ContainerGCPolicy, allSourcesReady bool, evictNonDeletedPods bool) error {
	// Separate containers by evict units.
	evictUnits, err := cgc.evictableContainers(gcPolicy.MinAge)
	if err != nil {
		return err
	}

	// Remove deleted pod containers if all sources are ready.
	if allSourcesReady {
		for key, unit := range evictUnits {
			if cgc.podDeletionProvider.IsPodDeleted(key.uid) || evictNonDeletedPods {
				cgc.removeOldestN(unit, len(unit)) // Remove all.
				delete(evictUnits, key)
			}
		}
	}

	// Enforce max containers per evict unit.
	if gcPolicy.MaxPerPodContainer >= 0 {
		cgc.enforceMaxContainersPerEvictUnit(evictUnits, gcPolicy.MaxPerPodContainer)
	}

	// Enforce max total number of containers.
	if gcPolicy.MaxContainers >= 0 && evictUnits.NumContainers() > gcPolicy.MaxContainers {
		// Leave an equal number of containers per evict unit (min: 1).
		numContainersPerEvictUnit := gcPolicy.MaxContainers / evictUnits.NumEvictUnits()
		if numContainersPerEvictUnit < 1 {
			numContainersPerEvictUnit = 1
		}
		cgc.enforceMaxContainersPerEvictUnit(evictUnits, numContainersPerEvictUnit)

		// If we still need to evict, evict oldest first.
		numContainers := evictUnits.NumContainers()
		if numContainers > gcPolicy.MaxContainers {
			flattened := make([]containerGCInfo, 0, numContainers)
			for key := range evictUnits {
				flattened = append(flattened, evictUnits[key]...)
			}
			sort.Sort(byCreated(flattened))

			cgc.removeOldestN(flattened, numContainers-gcPolicy.MaxContainers)
		}
	}
	return nil
}

// evictSandboxes remove all evictable sandboxes. An evictable sandbox must
// meet the following requirements:
//   1. not in ready state
//   2. contains no containers.
//   3. belong to a non-existent (i.e., already removed) pod, or is not the
//      most recently created sandbox for the pod.
func (cgc *containerGC) evictSandboxes(evictNonDeletedPods bool) error {
	containers, err := cgc.manager.getKubeletContainers(true)
	if err != nil {
		return err
	}

	// collect all the PodSandboxId of container
	sandboxIDs := sets.NewString()
	for _, container := range containers {
		sandboxIDs.Insert(container.PodSandboxId)
	}

	sandboxes, err := cgc.manager.getKubeletSandboxes(true)
	if err != nil {
		return err
	}

	sandboxesByPod := make(sandboxesByPodUID)
	for _, sandbox := range sandboxes {
		podUID := types.UID(sandbox.Metadata.Uid)
		sandboxInfo := sandboxGCInfo{
			id:         sandbox.Id,
			createTime: time.Unix(0, sandbox.CreatedAt),
		}

		// Set ready sandboxes to be active.
		if sandbox.State == runtimeapi.PodSandboxState_SANDBOX_READY {
			sandboxInfo.active = true
		}

		// Set sandboxes that still have containers to be active.
		if sandboxIDs.Has(sandbox.Id) {
			sandboxInfo.active = true
		}

		sandboxesByPod[podUID] = append(sandboxesByPod[podUID], sandboxInfo)
	}

	// Sort the sandboxes by age.
	for uid := range sandboxesByPod {
		sort.Sort(sandboxByCreated(sandboxesByPod[uid]))
	}

	for podUID, sandboxes := range sandboxesByPod {
		if cgc.podDeletionProvider.IsPodDeleted(podUID) || evictNonDeletedPods {
			// Remove all evictable sandboxes if the pod has been removed.
			// Note that the latest dead sandbox is also removed if there is
			// already an active one.
			cgc.removeOldestNSandboxes(sandboxes, len(sandboxes))
		} else {
			// Keep latest one if the pod still exists.
			cgc.removeOldestNSandboxes(sandboxes, len(sandboxes)-1)
		}
	}
	return nil
}

// evictPodLogsDirectories evicts all evictable pod logs directories. Pod logs directories
// are evictable if there are no corresponding pods.
func (cgc *containerGC) evictPodLogsDirectories(allSourcesReady bool) error {
	osInterface := cgc.manager.osInterface
	if allSourcesReady {
		// Only remove pod logs directories when all sources are ready.
		dirs, err := osInterface.ReadDir(podLogsRootDirectory)
		if err != nil {
			return fmt.Errorf("failed to read podLogsRootDirectory %q: %v", podLogsRootDirectory, err)
		}
		for _, dir := range dirs {
			name := dir.Name()
			podUID := types.UID(name)
			if !cgc.podDeletionProvider.IsPodDeleted(podUID) {
				continue
			}
			err := osInterface.RemoveAll(filepath.Join(podLogsRootDirectory, name))
			if err != nil {
				glog.Errorf("Failed to remove pod logs directory %q: %v", name, err)
			}
		}
	}

	// Remove dead container log symlinks.
	// TODO(random-liu): Remove this after cluster logging supports CRI container log path.
	logSymlinks, _ := osInterface.Glob(filepath.Join(legacyContainerLogsDir, fmt.Sprintf("*.%s", legacyLogSuffix)))
	for _, logSymlink := range logSymlinks {
		if _, err := osInterface.Stat(logSymlink); os.IsNotExist(err) {
			err := osInterface.Remove(logSymlink)
			if err != nil {
				glog.Errorf("Failed to remove container log dead symlink %q: %v", logSymlink, err)
			}
		}
	}
	return nil
}

// GarbageCollect removes dead containers using the specified container gc policy.
// Note that gc policy is not applied to sandboxes. Sandboxes are only removed when they are
// not ready and containing no containers.
//
// GarbageCollect consists of the following steps:
// * gets evictable containers which are not active and created more than gcPolicy.MinAge ago.
// * removes oldest dead containers for each pod by enforcing gcPolicy.MaxPerPodContainer.
// * removes oldest dead containers by enforcing gcPolicy.MaxContainers.
// * gets evictable sandboxes which are not ready and contains no containers.
// * removes evictable sandboxes.
func (cgc *containerGC) GarbageCollect(gcPolicy kubecontainer.ContainerGCPolicy, allSourcesReady bool, evictNonDeletedPods bool) error {
	// Remove evictable containers
	if err := cgc.evictContainers(gcPolicy, allSourcesReady, evictNonDeletedPods); err != nil {
		return err
	}

	// Remove sandboxes with zero containers
	if err := cgc.evictSandboxes(evictNonDeletedPods); err != nil {
		return err
	}

	// Remove pod sandbox log directory
	return cgc.evictPodLogsDirectories(allSourcesReady)
}
<|endoftext|>


<commit_before>// Package regex consumes logs given user-defined regex format for lines

// RE2 regex syntax reference: https://github.com/google/re2/wiki/Syntax
// Example format for a named capture group: `(?P<name>re)`

package regex

import (
	"errors"
	"fmt"
	"regexp"
	"strings"
	"sync"

	"github.com/sirupsen/logrus"

	"github.com/honeycombio/honeytail/event"
	"github.com/honeycombio/honeytail/httime"
	"github.com/honeycombio/honeytail/parsers"
)

type Options struct {
	// Note: `LineRegex` and `line_regex` are named as singular so that
	// it's less confusing to users to input them.
	// Might be worth making this consistent across the entire repo
	LineRegex       []string `long:"line_regex" description:"Regular expression with named capture groups representing the fields you want parsed (RE2 syntax). You can enter multiple regexes to match (--regex.line_regex=\"(?P<foo>re)\" --regex.line_regex=\"(?P<bar>...)\"). Parses using the first regex to match a line, so list them in most-to-least-specific order."`
	TimeFieldName   string   `long:"timefield" description:"Name of the field that contains a timestamp"`
	TimeFieldFormat string   `long:"time_format" description:"Timestamp format to use (strftime and Golang time.Parse supported)"`
	NumParsers      int      `hidden:"true" description:"number of regex parsers to spin up"`
}

type Parser struct {
	conf       Options
	lineParser parsers.LineParser
}

func (p *Parser) Init(options interface{}) error {
	p.conf = *options.(*Options)
	if len(p.conf.LineRegex) == 0 {
		return errors.New("Must provide at least one regex for parsing log lines; use `--regex.line_regex` flag.")
	}
	lineParser, err := NewRegexLineParser(p.conf.LineRegex)
	if err != nil {
		return err
	}
	p.lineParser = lineParser
	return nil
}

// Compile multiple log line regexes
func ParseLineRegexes(regexStrs []string) ([]*regexp.Regexp, error) {
	regexes := make([]*regexp.Regexp, 0)
	for _, regexStr := range regexStrs {
		regex, err := ParseLineRegex(regexStr)
		if err != nil {
			return regexes, err
		}
		regexes = append(regexes, regex)
	}
	return regexes, nil
}

// Compile a regex & validate expectations for log line parsing
func ParseLineRegex(regexStr string) (*regexp.Regexp, error) {
	// Regex can't be blank
	if regexStr == "" {
		logrus.Debug("LineRegex is blank; required field")
		return nil, errors.New("Must provide a regex for parsing log lines; use `--regex.line_regex` flag.")
	}

	// Compile regex
	lineRegex, err := regexp.Compile(regexStr)
	if err != nil {
		logrus.WithFields(logrus.Fields{
			"lineRegex": regexStr,
		}).Error("Could not compile line regex")
		return nil, err
	}

	// Require at least one named group
	var numNamedGroups int
	for _, groupName := range lineRegex.SubexpNames() {
		if groupName != "" {
			numNamedGroups++
		}
	}
	if numNamedGroups == 0 {
		logrus.WithFields(logrus.Fields{
			"LineRegex": regexStr,
		}).Error("No named capture groups")
		return nil, errors.New(fmt.Sprintf("No named capture groups found in regex: '%s'. Must provide at least one named group with line regex. Example: `(?P<name>re)`", regexStr))
	}

	return lineRegex, nil
}

/* LineParser for regexes */

type RegexLineParser struct {
	lineRegexes []*regexp.Regexp
}

// RegexLineParser factory
func NewRegexLineParser(regexStrs []string) (*RegexLineParser, error) {
	lineRegexes, err := ParseLineRegexes(regexStrs)
	if err != nil {
		return nil, err
	}
	logrus.WithFields(logrus.Fields{
		"lineRegexes": lineRegexes,
	}).Debug("Compiled line regexes")
	return &RegexLineParser{lineRegexes}, nil
}

func (p *RegexLineParser) ParseLine(line string) (map[string]interface{}, error) {
	for _, lineRegex := range p.lineRegexes {
		parsed := make(map[string]interface{})
		match := lineRegex.FindAllStringSubmatch(line, -1)
		if match == nil || len(match) == 0 {
			logrus.WithFields(logrus.Fields{
				"line":      line,
				"lineRegex": lineRegex,
			}).Debug("No matches for regex log line")
			continue // No matches found, skip to next regex
		}

		// Map capture groups
		var firstMatch []string = match[0] // We only care about the first full lineRegex match
		for i, name := range lineRegex.SubexpNames() {
			if i != 0 && i < len(firstMatch) {
				parsed[name] = firstMatch[i]
			}
		}
		logrus.WithFields(logrus.Fields{
			"parsed":    parsed,
			"line":      line,
			"lineRegex": lineRegex,
		}).Debug("Regex parsing log line")

		return parsed, nil
	}
	return make(map[string]interface{}), nil
}

func (p *Parser) ProcessLines(lines <-chan string, send chan<- event.Event, prefixRegex *parsers.ExtRegexp) {
	// parse lines one by one
	wg := sync.WaitGroup{}
	numParsers := 1
	if p.conf.NumParsers > 0 {
		numParsers = p.conf.NumParsers
	}
	for i := 0; i < numParsers; i++ {
		wg.Add(1)
		go func() {
			for line := range lines {
				logrus.WithFields(logrus.Fields{
					"line": line,
				}).Debug("Attempting to process regex log line")

				// take care of any headers on the line
				var prefixFields map[string]string
				if prefixRegex != nil {
					var prefix string
					prefix, prefixFields = prefixRegex.FindStringSubmatchMap(line)
					line = strings.TrimPrefix(line, prefix)
				}

				parsedLine, err := p.lineParser.ParseLine(line)
				if err != nil {
					continue
				}

				// merge the prefix fields and the parsed line contents
				for k, v := range prefixFields {
					parsedLine[k] = v
				}

				if len(parsedLine) == 0 {
					logrus.WithFields(logrus.Fields{
						"line": line,
					}).Debug("Skipping line; no capture groups found")
					continue
				}

				// look for the timestamp in any of the prefix fields or regular content
				timestamp := httime.GetTimestamp(parsedLine, p.conf.TimeFieldName, p.conf.TimeFieldFormat)

				// send an event to Transmission
				e := event.Event{
					Timestamp: timestamp,
					Data:      parsedLine,
				}
				send <- e
			}
			wg.Done()
		}()
	}
	wg.Wait()
	logrus.Debug("lines channel is closed, ending regex processor")
}
<commit_msg>Fix function comments based on best practices from Effective Go<commit_after>// Package regex consumes logs given user-defined regex format for lines

// RE2 regex syntax reference: https://github.com/google/re2/wiki/Syntax
// Example format for a named capture group: `(?P<name>re)`

package regex

import (
	"errors"
	"fmt"
	"regexp"
	"strings"
	"sync"

	"github.com/sirupsen/logrus"

	"github.com/honeycombio/honeytail/event"
	"github.com/honeycombio/honeytail/httime"
	"github.com/honeycombio/honeytail/parsers"
)

type Options struct {
	// Note: `LineRegex` and `line_regex` are named as singular so that
	// it's less confusing to users to input them.
	// Might be worth making this consistent across the entire repo
	LineRegex       []string `long:"line_regex" description:"Regular expression with named capture groups representing the fields you want parsed (RE2 syntax). You can enter multiple regexes to match (--regex.line_regex=\"(?P<foo>re)\" --regex.line_regex=\"(?P<bar>...)\"). Parses using the first regex to match a line, so list them in most-to-least-specific order."`
	TimeFieldName   string   `long:"timefield" description:"Name of the field that contains a timestamp"`
	TimeFieldFormat string   `long:"time_format" description:"Timestamp format to use (strftime and Golang time.Parse supported)"`
	NumParsers      int      `hidden:"true" description:"number of regex parsers to spin up"`
}

type Parser struct {
	conf       Options
	lineParser parsers.LineParser
}

func (p *Parser) Init(options interface{}) error {
	p.conf = *options.(*Options)
	if len(p.conf.LineRegex) == 0 {
		return errors.New("Must provide at least one regex for parsing log lines; use `--regex.line_regex` flag.")
	}
	lineParser, err := NewRegexLineParser(p.conf.LineRegex)
	if err != nil {
		return err
	}
	p.lineParser = lineParser
	return nil
}

// Compile multiple log line regexes
func ParseLineRegexes(regexStrs []string) ([]*regexp.Regexp, error) {
	regexes := make([]*regexp.Regexp, 0)
	for _, regexStr := range regexStrs {
		regex, err := ParseLineRegex(regexStr)
		if err != nil {
			return regexes, err
		}
		regexes = append(regexes, regex)
	}
	return regexes, nil
}

// Compile a regex & validate expectations for log line parsing
func ParseLineRegex(regexStr string) (*regexp.Regexp, error) {
	// Regex can't be blank
	if regexStr == "" {
		logrus.Debug("LineRegex is blank; required field")
		return nil, errors.New("Must provide a regex for parsing log lines; use `--regex.line_regex` flag.")
	}

	// Compile regex
	lineRegex, err := regexp.Compile(regexStr)
	if err != nil {
		logrus.WithFields(logrus.Fields{
			"lineRegex": regexStr,
		}).Error("Could not compile line regex")
		return nil, err
	}

	// Require at least one named group
	var numNamedGroups int
	for _, groupName := range lineRegex.SubexpNames() {
		if groupName != "" {
			numNamedGroups++
		}
	}
	if numNamedGroups == 0 {
		logrus.WithFields(logrus.Fields{
			"LineRegex": regexStr,
		}).Error("No named capture groups")
		return nil, errors.New(fmt.Sprintf("No named capture groups found in regex: '%s'. Must provide at least one named group with line regex. Example: `(?P<name>re)`", regexStr))
	}

	return lineRegex, nil
}

/* LineParser for regexes */

type RegexLineParser struct {
	lineRegexes []*regexp.Regexp
}

// NewRegexLineParser factory
func NewRegexLineParser(regexStrs []string) (*RegexLineParser, error) {
	lineRegexes, err := ParseLineRegexes(regexStrs)
	if err != nil {
		return nil, err
	}
	logrus.WithFields(logrus.Fields{
		"lineRegexes": lineRegexes,
	}).Debug("Compiled line regexes")
	return &RegexLineParser{lineRegexes}, nil
}

func (p *RegexLineParser) ParseLine(line string) (map[string]interface{}, error) {
	for _, lineRegex := range p.lineRegexes {
		parsed := make(map[string]interface{})
		match := lineRegex.FindAllStringSubmatch(line, -1)
		if match == nil || len(match) == 0 {
			logrus.WithFields(logrus.Fields{
				"line":      line,
				"lineRegex": lineRegex,
			}).Debug("No matches for regex log line")
			continue // No matches found, skip to next regex
		}

		// Map capture groups
		var firstMatch []string = match[0] // We only care about the first full lineRegex match
		for i, name := range lineRegex.SubexpNames() {
			if i != 0 && i < len(firstMatch) {
				parsed[name] = firstMatch[i]
			}
		}
		logrus.WithFields(logrus.Fields{
			"parsed":    parsed,
			"line":      line,
			"lineRegex": lineRegex,
		}).Debug("Regex parsing log line")

		return parsed, nil
	}
	return make(map[string]interface{}), nil
}

func (p *Parser) ProcessLines(lines <-chan string, send chan<- event.Event, prefixRegex *parsers.ExtRegexp) {
	// parse lines one by one
	wg := sync.WaitGroup{}
	numParsers := 1
	if p.conf.NumParsers > 0 {
		numParsers = p.conf.NumParsers
	}
	for i := 0; i < numParsers; i++ {
		wg.Add(1)
		go func() {
			for line := range lines {
				logrus.WithFields(logrus.Fields{
					"line": line,
				}).Debug("Attempting to process regex log line")

				// take care of any headers on the line
				var prefixFields map[string]string
				if prefixRegex != nil {
					var prefix string
					prefix, prefixFields = prefixRegex.FindStringSubmatchMap(line)
					line = strings.TrimPrefix(line, prefix)
				}

				parsedLine, err := p.lineParser.ParseLine(line)
				if err != nil {
					continue
				}

				// merge the prefix fields and the parsed line contents
				for k, v := range prefixFields {
					parsedLine[k] = v
				}

				if len(parsedLine) == 0 {
					logrus.WithFields(logrus.Fields{
						"line": line,
					}).Debug("Skipping line; no capture groups found")
					continue
				}

				// look for the timestamp in any of the prefix fields or regular content
				timestamp := httime.GetTimestamp(parsedLine, p.conf.TimeFieldName, p.conf.TimeFieldFormat)

				// send an event to Transmission
				e := event.Event{
					Timestamp: timestamp,
					Data:      parsedLine,
				}
				send <- e
			}
			wg.Done()
		}()
	}
	wg.Wait()
	logrus.Debug("lines channel is closed, ending regex processor")
}
<|endoftext|>


<commit_before>/*
Copyright 2018 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package etcdmanager

import (
	"encoding/json"
	"fmt"
	"strings"

	"k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	"k8s.io/kops/pkg/apis/kops"
	"k8s.io/kops/pkg/assets"
	"k8s.io/kops/pkg/dns"
	"k8s.io/kops/pkg/flagbuilder"
	"k8s.io/kops/pkg/k8scodecs"
	"k8s.io/kops/pkg/kubemanifest"
	"k8s.io/kops/pkg/model"
	"k8s.io/kops/upup/pkg/fi"
	"k8s.io/kops/upup/pkg/fi/cloudup/awsup"
	"k8s.io/kops/upup/pkg/fi/fitasks"
	"k8s.io/kops/util/pkg/exec"
)

const metaFilename = "_etcd_backup.meta"

// EtcdManagerBuilder builds the manifest for the etcd-manager
type EtcdManagerBuilder struct {
	*model.KopsModelContext
	Lifecycle    *fi.Lifecycle
	AssetBuilder *assets.AssetBuilder
}

var _ fi.ModelBuilder = &EtcdManagerBuilder{}

// Build creates the tasks
func (b *EtcdManagerBuilder) Build(c *fi.ModelBuilderContext) error {
	for _, etcdCluster := range b.Cluster.Spec.EtcdClusters {
		if etcdCluster.Manager == nil {
			continue
		}

		name := etcdCluster.Name
		version := etcdCluster.Version

		backupStore := ""
		if etcdCluster.Backups != nil {
			backupStore = etcdCluster.Backups.BackupStore
		}
		if backupStore == "" {
			return fmt.Errorf("backupStore must be set for use with etcd-manager")
		}

		manifest, err := b.buildManifest(etcdCluster)
		if err != nil {
			return err
		}

		manifestYAML, err := k8scodecs.ToVersionedYaml(manifest)
		if err != nil {
			return fmt.Errorf("error marshalling manifest to yaml: %v", err)
		}

		c.AddTask(&fitasks.ManagedFile{
			Contents:  fi.WrapResource(fi.NewBytesResource(manifestYAML)),
			Lifecycle: b.Lifecycle,
			Location:  fi.String("manifests/etcd/" + name + ".yaml"),
			Name:      fi.String("manifests-etcdmanager-" + name),
		})

		info := &etcdClusterSpec{
			EtcdVersion: version,
			MemberCount: int32(len(etcdCluster.Members)),
		}

		d, err := json.MarshalIndent(info, "", "  ")
		if err != nil {
			return err
		}

		c.AddTask(&fitasks.ManagedFile{
			Contents:  fi.WrapResource(fi.NewBytesResource(d)),
			Lifecycle: b.Lifecycle,
			// TODO: We need this to match the backup base (currently)
			Location: fi.String("backups/etcd/" + etcdCluster.Name + "/control/etcd-cluster-spec"),
			Name:     fi.String("etcd-cluster-spec-" + name),
		})
	}

	return nil
}

type etcdClusterSpec struct {
	MemberCount int32  `json:"member_count,omitempty"`
	EtcdVersion string `json:"etcd_version,omitempty"`
}

func (b *EtcdManagerBuilder) buildManifest(etcdCluster *kops.EtcdClusterSpec) (*v1.Pod, error) {
	if etcdCluster.Manager == nil {
		return nil, fmt.Errorf("manager not set for EtcdCluster")
	}

	return b.buildPod(etcdCluster)
}

// BuildEtcdManifest creates the pod spec, based on the etcd cluster
func (b *EtcdManagerBuilder) buildPod(etcdCluster *kops.EtcdClusterSpec) (*v1.Pod, error) {
	image := etcdCluster.Manager.Image
	{
		remapped, err := b.AssetBuilder.RemapImage(image)
		if err != nil {
			return nil, fmt.Errorf("unable to remap container %q: %v", image, err)
		} else {
			image = remapped
		}
	}

	isTLS := etcdCluster.EnableEtcdTLS

	cpuRequest := resource.MustParse("100m")
	clientPort := 4001

	clusterName := "etcd-" + etcdCluster.Name
	peerPort := 2380
	backupStore := ""
	if etcdCluster.Backups != nil {
		backupStore = etcdCluster.Backups.BackupStore
	}

	podName := "etcd-manager-" + etcdCluster.Name

	// TODO: Use a socket file for the quarantine port
	quarantinedClientPort := 3994

	grpcPort := 3996

	// The dns suffix logic mirrors the existing logic, so we should be compatible with existing clusters
	// (etcd makes it difficult to change peer urls, treating it as a cluster event, for reasons unknown)
	dnsInternalSuffix := ""
	if dns.IsGossipHostname(b.Cluster.Spec.MasterInternalName) {
		// @TODO: This is hacky, but we want it so that we can have a different internal & external name
		dnsInternalSuffix = b.Cluster.Spec.MasterInternalName
		dnsInternalSuffix = strings.TrimPrefix(dnsInternalSuffix, "api.")
	}

	if dnsInternalSuffix == "" {
		dnsInternalSuffix = ".internal." + b.Cluster.ObjectMeta.Name
	}

	switch etcdCluster.Name {
	case "main":
		clusterName = "etcd"
		cpuRequest = resource.MustParse("200m")

	case "events":
		clientPort = 4002
		peerPort = 2381
		grpcPort = 3997
		quarantinedClientPort = 3995

	default:
		return nil, fmt.Errorf("unknown etcd cluster key %q", etcdCluster.Name)
	}

	if backupStore == "" {
		return nil, fmt.Errorf("backupStore must be set for use with etcd-manager")
	}

	name := clusterName
	if !strings.HasPrefix(name, "etcd") {
		// For sanity, and to avoid collisions in directories / dns
		return nil, fmt.Errorf("unexpected name for etcd cluster (must start with etcd): %q", name)
	}
	logFile := "/var/log/" + name + ".log"

	config := &EtcdManagerConfig{
		Containerized: true,
		ClusterName:   clusterName,
		BackupStore:   backupStore,
		GrpcPort:      grpcPort,
		DNSSuffix:     dnsInternalSuffix,
	}

	var envs []v1.EnvVar

	{
		// @check if we are using TLS
		scheme := "http"
		if isTLS {
			scheme = "https"
		}

		config.PeerUrls = fmt.Sprintf("%s://__name__:%d", scheme, peerPort)
		config.ClientUrls = fmt.Sprintf("%s://__name__:%d", scheme, clientPort)
		config.QuarantineClientUrls = fmt.Sprintf("%s://__name__:%d", scheme, quarantinedClientPort)

		// TODO: We need to wire these into the etcd-manager spec
		// // add timeout/heartbeat settings
		if etcdCluster.LeaderElectionTimeout != nil {
			// 	envs = append(envs, v1.EnvVar{Name: "ETCD_ELECTION_TIMEOUT", Value: convEtcdSettingsToMs(etcdClusterSpec.LeaderElectionTimeout)})
			return nil, fmt.Errorf("LeaderElectionTimeout not supported by etcd-manager")
		}
		if etcdCluster.HeartbeatInterval != nil {
			// 	envs = append(envs, v1.EnvVar{Name: "ETCD_HEARTBEAT_INTERVAL", Value: convEtcdSettingsToMs(etcdClusterSpec.HeartbeatInterval)})
			return nil, fmt.Errorf("HeartbeatInterval not supported by etcd-manager")
		}

		if isTLS {
			return nil, fmt.Errorf("TLS not supported for etcd-manager")
		}
	}

	{
		switch kops.CloudProviderID(b.Cluster.Spec.CloudProvider) {
		case kops.CloudProviderAWS:
			config.VolumeProvider = "aws"

			config.VolumeTag = []string{
				fmt.Sprintf("kubernetes.io/cluster/%s=owned", b.Cluster.Name),
				awsup.TagNameEtcdClusterPrefix + etcdCluster.Name,
				awsup.TagNameRolePrefix + "master=1",
			}
			config.VolumeNameTag = awsup.TagNameEtcdClusterPrefix + etcdCluster.Name

		default:
			return nil, fmt.Errorf("CloudProvider %q not supported with etcd-manager", b.Cluster.Spec.CloudProvider)
		}
	}

	args, err := flagbuilder.BuildFlagsList(config)
	if err != nil {
		return nil, err
	}

	pod := &v1.Pod{}
	pod.APIVersion = "v1"
	pod.Kind = "Pod"
	pod.Name = podName
	pod.Namespace = "kube-system"
	pod.Labels = map[string]string{"k8s-app": podName}
	pod.Spec.HostNetwork = true

	{
		container := &v1.Container{
			Name:  "etcd-manager",
			Image: image,
			Resources: v1.ResourceRequirements{
				Requests: v1.ResourceList{
					v1.ResourceCPU: cpuRequest,
				},
			},
			Command: exec.WithTee("/etcd-manager", args, "/var/log/etcd.log"),
			Env:     envs,
		}

		// TODO: Reduce these permissions (they are needed for volume mounting)
		container.SecurityContext = &v1.SecurityContext{
			Privileged: fi.Bool(true),
		}

		// TODO: Use helper function here
		container.VolumeMounts = append(container.VolumeMounts, v1.VolumeMount{
			Name:      "varlogetcd",
			MountPath: "/var/log/etcd.log",
			ReadOnly:  false,
		})

		// TODO: Would be nice to narrow this mount
		container.VolumeMounts = append(container.VolumeMounts, v1.VolumeMount{
			Name:      "rootfs",
			MountPath: "/rootfs",
			ReadOnly:  false,
		})
		hostPathFileOrCreate := v1.HostPathFileOrCreate
		pod.Spec.Volumes = append(pod.Spec.Volumes, v1.Volume{
			Name: "varlogetcd",
			VolumeSource: v1.VolumeSource{
				HostPath: &v1.HostPathVolumeSource{
					Path: logFile,
					Type: &hostPathFileOrCreate,
				},
			},
		})

		hostPathDirectory := v1.HostPathDirectory
		pod.Spec.Volumes = append(pod.Spec.Volumes, v1.Volume{
			Name: "rootfs",
			VolumeSource: v1.VolumeSource{
				HostPath: &v1.HostPathVolumeSource{
					Path: "/",
					Type: &hostPathDirectory,
				},
			},
		})

		kubemanifest.MapEtcHosts(pod, container, false)

		if isTLS {
			return nil, fmt.Errorf("TLS not supported for etcd-manager")
		}

		pod.Spec.Containers = append(pod.Spec.Containers, *container)
	}

	kubemanifest.MarkPodAsCritical(pod)

	return pod, nil
}

// EtcdManagerConfig are the flags for etcd-manager
type EtcdManagerConfig struct {
	// Containerized is set if etcd-manager is running in a container
	Containerized bool `flag:"containerized"`

	Address              string   `flag:"address"`
	PeerUrls             string   `flag:"peer-urls"`
	GrpcPort             int      `flag:"grpc-port"`
	ClientUrls           string   `flag:"client-urls"`
	QuarantineClientUrls string   `flag:"quarantine-client-urls"`
	ClusterName          string   `flag:"cluster-name"`
	BackupStore          string   `flag:"backup-store"`
	DataDir              string   `flag:"data-dir"`
	VolumeProvider       string   `flag:"volume-provider"`
	VolumeTag            []string `flag:"volume-tag,repeat"`
	VolumeNameTag        string   `flag:"volume-name-tag"`
	DNSSuffix            string   `flag:"dns-suffix"`
}
<commit_msg>Set log-verbosity for etcd-manager<commit_after>/*
Copyright 2018 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package etcdmanager

import (
	"encoding/json"
	"fmt"
	"strings"

	"k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	"k8s.io/kops/pkg/apis/kops"
	"k8s.io/kops/pkg/assets"
	"k8s.io/kops/pkg/dns"
	"k8s.io/kops/pkg/flagbuilder"
	"k8s.io/kops/pkg/k8scodecs"
	"k8s.io/kops/pkg/kubemanifest"
	"k8s.io/kops/pkg/model"
	"k8s.io/kops/upup/pkg/fi"
	"k8s.io/kops/upup/pkg/fi/cloudup/awsup"
	"k8s.io/kops/upup/pkg/fi/fitasks"
	"k8s.io/kops/util/pkg/exec"
)

const metaFilename = "_etcd_backup.meta"

// EtcdManagerBuilder builds the manifest for the etcd-manager
type EtcdManagerBuilder struct {
	*model.KopsModelContext
	Lifecycle    *fi.Lifecycle
	AssetBuilder *assets.AssetBuilder
}

var _ fi.ModelBuilder = &EtcdManagerBuilder{}

// Build creates the tasks
func (b *EtcdManagerBuilder) Build(c *fi.ModelBuilderContext) error {
	for _, etcdCluster := range b.Cluster.Spec.EtcdClusters {
		if etcdCluster.Manager == nil {
			continue
		}

		name := etcdCluster.Name
		version := etcdCluster.Version

		backupStore := ""
		if etcdCluster.Backups != nil {
			backupStore = etcdCluster.Backups.BackupStore
		}
		if backupStore == "" {
			return fmt.Errorf("backupStore must be set for use with etcd-manager")
		}

		manifest, err := b.buildManifest(etcdCluster)
		if err != nil {
			return err
		}

		manifestYAML, err := k8scodecs.ToVersionedYaml(manifest)
		if err != nil {
			return fmt.Errorf("error marshalling manifest to yaml: %v", err)
		}

		c.AddTask(&fitasks.ManagedFile{
			Contents:  fi.WrapResource(fi.NewBytesResource(manifestYAML)),
			Lifecycle: b.Lifecycle,
			Location:  fi.String("manifests/etcd/" + name + ".yaml"),
			Name:      fi.String("manifests-etcdmanager-" + name),
		})

		info := &etcdClusterSpec{
			EtcdVersion: version,
			MemberCount: int32(len(etcdCluster.Members)),
		}

		d, err := json.MarshalIndent(info, "", "  ")
		if err != nil {
			return err
		}

		c.AddTask(&fitasks.ManagedFile{
			Contents:  fi.WrapResource(fi.NewBytesResource(d)),
			Lifecycle: b.Lifecycle,
			// TODO: We need this to match the backup base (currently)
			Location: fi.String("backups/etcd/" + etcdCluster.Name + "/control/etcd-cluster-spec"),
			Name:     fi.String("etcd-cluster-spec-" + name),
		})
	}

	return nil
}

type etcdClusterSpec struct {
	MemberCount int32  `json:"member_count,omitempty"`
	EtcdVersion string `json:"etcd_version,omitempty"`
}

func (b *EtcdManagerBuilder) buildManifest(etcdCluster *kops.EtcdClusterSpec) (*v1.Pod, error) {
	if etcdCluster.Manager == nil {
		return nil, fmt.Errorf("manager not set for EtcdCluster")
	}

	return b.buildPod(etcdCluster)
}

// BuildEtcdManifest creates the pod spec, based on the etcd cluster
func (b *EtcdManagerBuilder) buildPod(etcdCluster *kops.EtcdClusterSpec) (*v1.Pod, error) {
	image := etcdCluster.Manager.Image
	{
		remapped, err := b.AssetBuilder.RemapImage(image)
		if err != nil {
			return nil, fmt.Errorf("unable to remap container %q: %v", image, err)
		} else {
			image = remapped
		}
	}

	isTLS := etcdCluster.EnableEtcdTLS

	cpuRequest := resource.MustParse("100m")
	clientPort := 4001

	clusterName := "etcd-" + etcdCluster.Name
	peerPort := 2380
	backupStore := ""
	if etcdCluster.Backups != nil {
		backupStore = etcdCluster.Backups.BackupStore
	}

	podName := "etcd-manager-" + etcdCluster.Name

	// TODO: Use a socket file for the quarantine port
	quarantinedClientPort := 3994

	grpcPort := 3996

	// The dns suffix logic mirrors the existing logic, so we should be compatible with existing clusters
	// (etcd makes it difficult to change peer urls, treating it as a cluster event, for reasons unknown)
	dnsInternalSuffix := ""
	if dns.IsGossipHostname(b.Cluster.Spec.MasterInternalName) {
		// @TODO: This is hacky, but we want it so that we can have a different internal & external name
		dnsInternalSuffix = b.Cluster.Spec.MasterInternalName
		dnsInternalSuffix = strings.TrimPrefix(dnsInternalSuffix, "api.")
	}

	if dnsInternalSuffix == "" {
		dnsInternalSuffix = ".internal." + b.Cluster.ObjectMeta.Name
	}

	switch etcdCluster.Name {
	case "main":
		clusterName = "etcd"
		cpuRequest = resource.MustParse("200m")

	case "events":
		clientPort = 4002
		peerPort = 2381
		grpcPort = 3997
		quarantinedClientPort = 3995

	default:
		return nil, fmt.Errorf("unknown etcd cluster key %q", etcdCluster.Name)
	}

	if backupStore == "" {
		return nil, fmt.Errorf("backupStore must be set for use with etcd-manager")
	}

	name := clusterName
	if !strings.HasPrefix(name, "etcd") {
		// For sanity, and to avoid collisions in directories / dns
		return nil, fmt.Errorf("unexpected name for etcd cluster (must start with etcd): %q", name)
	}
	logFile := "/var/log/" + name + ".log"

	config := &EtcdManagerConfig{
		Containerized: true,
		ClusterName:   clusterName,
		BackupStore:   backupStore,
		GrpcPort:      grpcPort,
		DNSSuffix:     dnsInternalSuffix,
	}

	config.LogVerbosity = 8

	var envs []v1.EnvVar

	{
		// @check if we are using TLS
		scheme := "http"
		if isTLS {
			scheme = "https"
		}

		config.PeerUrls = fmt.Sprintf("%s://__name__:%d", scheme, peerPort)
		config.ClientUrls = fmt.Sprintf("%s://__name__:%d", scheme, clientPort)
		config.QuarantineClientUrls = fmt.Sprintf("%s://__name__:%d", scheme, quarantinedClientPort)

		// TODO: We need to wire these into the etcd-manager spec
		// // add timeout/heartbeat settings
		if etcdCluster.LeaderElectionTimeout != nil {
			// 	envs = append(envs, v1.EnvVar{Name: "ETCD_ELECTION_TIMEOUT", Value: convEtcdSettingsToMs(etcdClusterSpec.LeaderElectionTimeout)})
			return nil, fmt.Errorf("LeaderElectionTimeout not supported by etcd-manager")
		}
		if etcdCluster.HeartbeatInterval != nil {
			// 	envs = append(envs, v1.EnvVar{Name: "ETCD_HEARTBEAT_INTERVAL", Value: convEtcdSettingsToMs(etcdClusterSpec.HeartbeatInterval)})
			return nil, fmt.Errorf("HeartbeatInterval not supported by etcd-manager")
		}

		if isTLS {
			return nil, fmt.Errorf("TLS not supported for etcd-manager")
		}
	}

	{
		switch kops.CloudProviderID(b.Cluster.Spec.CloudProvider) {
		case kops.CloudProviderAWS:
			config.VolumeProvider = "aws"

			config.VolumeTag = []string{
				fmt.Sprintf("kubernetes.io/cluster/%s=owned", b.Cluster.Name),
				awsup.TagNameEtcdClusterPrefix + etcdCluster.Name,
				awsup.TagNameRolePrefix + "master=1",
			}
			config.VolumeNameTag = awsup.TagNameEtcdClusterPrefix + etcdCluster.Name

		default:
			return nil, fmt.Errorf("CloudProvider %q not supported with etcd-manager", b.Cluster.Spec.CloudProvider)
		}
	}

	args, err := flagbuilder.BuildFlagsList(config)
	if err != nil {
		return nil, err
	}

	pod := &v1.Pod{}
	pod.APIVersion = "v1"
	pod.Kind = "Pod"
	pod.Name = podName
	pod.Namespace = "kube-system"
	pod.Labels = map[string]string{"k8s-app": podName}
	pod.Spec.HostNetwork = true

	{
		container := &v1.Container{
			Name:  "etcd-manager",
			Image: image,
			Resources: v1.ResourceRequirements{
				Requests: v1.ResourceList{
					v1.ResourceCPU: cpuRequest,
				},
			},
			Command: exec.WithTee("/etcd-manager", args, "/var/log/etcd.log"),
			Env:     envs,
		}

		// TODO: Reduce these permissions (they are needed for volume mounting)
		container.SecurityContext = &v1.SecurityContext{
			Privileged: fi.Bool(true),
		}

		// TODO: Use helper function here
		container.VolumeMounts = append(container.VolumeMounts, v1.VolumeMount{
			Name:      "varlogetcd",
			MountPath: "/var/log/etcd.log",
			ReadOnly:  false,
		})

		// TODO: Would be nice to narrow this mount
		container.VolumeMounts = append(container.VolumeMounts, v1.VolumeMount{
			Name:      "rootfs",
			MountPath: "/rootfs",
			ReadOnly:  false,
		})
		hostPathFileOrCreate := v1.HostPathFileOrCreate
		pod.Spec.Volumes = append(pod.Spec.Volumes, v1.Volume{
			Name: "varlogetcd",
			VolumeSource: v1.VolumeSource{
				HostPath: &v1.HostPathVolumeSource{
					Path: logFile,
					Type: &hostPathFileOrCreate,
				},
			},
		})

		hostPathDirectory := v1.HostPathDirectory
		pod.Spec.Volumes = append(pod.Spec.Volumes, v1.Volume{
			Name: "rootfs",
			VolumeSource: v1.VolumeSource{
				HostPath: &v1.HostPathVolumeSource{
					Path: "/",
					Type: &hostPathDirectory,
				},
			},
		})

		kubemanifest.MapEtcHosts(pod, container, false)

		if isTLS {
			return nil, fmt.Errorf("TLS not supported for etcd-manager")
		}

		pod.Spec.Containers = append(pod.Spec.Containers, *container)
	}

	kubemanifest.MarkPodAsCritical(pod)

	return pod, nil
}

// EtcdManagerConfig are the flags for etcd-manager
type EtcdManagerConfig struct {
	// LogVerbosity sets the log verbosity level
	LogVerbosity int `flag:"v"`

	// Containerized is set if etcd-manager is running in a container
	Containerized bool `flag:"containerized"`

	Address              string   `flag:"address"`
	PeerUrls             string   `flag:"peer-urls"`
	GrpcPort             int      `flag:"grpc-port"`
	ClientUrls           string   `flag:"client-urls"`
	QuarantineClientUrls string   `flag:"quarantine-client-urls"`
	ClusterName          string   `flag:"cluster-name"`
	BackupStore          string   `flag:"backup-store"`
	DataDir              string   `flag:"data-dir"`
	VolumeProvider       string   `flag:"volume-provider"`
	VolumeTag            []string `flag:"volume-tag,repeat"`
	VolumeNameTag        string   `flag:"volume-name-tag"`
	DNSSuffix            string   `flag:"dns-suffix"`
}
<|endoftext|>