<commit_before>// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 gcsproxy

import (
	"fmt"
	"io"
	"io/ioutil"
	"math"
	"os"

	"github.com/jacobsa/gcloud/gcs"
	"golang.org/x/net/context"
	"google.golang.org/cloud/storage"
)

// A view on a particular generation of an object in GCS that allows random
// access reads and writes.
//
// Reads may involve reading from a local cache. Writes are buffered locally
// until the Sync method is called, at which time a new generation of the
// object is created.
//
// This type is not safe for concurrent access. The user must provide external
// synchronization.
type ObjectProxy struct {
	/////////////////////////
	// Dependencies
	/////////////////////////

	bucket gcs.Bucket

	/////////////////////////
	// Mutable state
	/////////////////////////

	// A record for the specific generation of the object from which our local
	// state is branched. If we have no local state, the contents of this
	// generation are exactly our contents.
	src storage.Object

	// A local temporary file containing our current contents. When non-nil, this
	// is the authority on our contents. When nil, our contents are defined by
	// 'src' above.
	localFile *os.File

	// true if localFile is present but its contents may be different from the
	// contents of our source generation. Sync needs to do work iff this is true.
	//
	// INVARIANT: If dirty, then localFile != nil
	dirty bool
}

////////////////////////////////////////////////////////////////////////
// Public interface
////////////////////////////////////////////////////////////////////////

// Create a view on the given GCS object generation which is assumed to have
// the given size, or zero if branching from a non-existent object (in which
// case the initial contents are empty).
//
// REQUIRES: If srcGeneration == 0, then srcSize == 0
func NewObjectProxy(
	ctx context.Context,
	bucket gcs.Bucket,
	name string,
	srcGeneration int64,
	srcSize int64) (op *ObjectProxy, err error) {
	// Set up the basic struct.
	op = &ObjectProxy{
		bucket:        bucket,
		name:          name,
		srcGeneration: srcGeneration,
		srcSize:       srcSize,
	}

	// For "doesn't exist" source generations, we must establish an empty local
	// file and mark the proxy dirty.
	if srcGeneration == 0 {
		if err = op.ensureLocalFile(ctx); err != nil {
			return
		}

		op.dirty = true
	}

	return
}

// Return the name of the proxied object. This may or may not be an object that
// currently exists in the bucket.
func (op *ObjectProxy) Name() string {
	return op.name
}

// Panic if any internal invariants are violated. Careful users can call this
// at appropriate times to help debug weirdness. Consider using
// syncutil.InvariantMutex to automate the process.
func (op *ObjectProxy) CheckInvariants() {
	// INVARIANT: If srcGeneration == 0, srcSize == 0
	if op.srcGeneration == 0 && op.srcSize != 0 {
		panic("Expected zero source size.")
	}

	// INVARIANT: If srcGeneration == 0, then dirty
	if op.srcGeneration == 0 && !op.dirty {
		panic("Expected dirty.")
	}

	// INVARIANT: If dirty, then localFile != nil
	if op.dirty && op.localFile == nil {
		panic("Expected non-nil localFile.")
	}
}

// Destroy any local file caches, putting the proxy into an indeterminate
// state. Should be used before dropping the final reference to the proxy.
func (op *ObjectProxy) Destroy() (err error) {
	// Make sure that when we exit no invariants are violated.
	defer func() {
		op.srcGeneration = 1
		op.localFile = nil
		op.dirty = false
	}()

	// If we have no local file, there's nothing to do.
	if op.localFile == nil {
		return
	}

	// Close the local file.
	if err = op.localFile.Close(); err != nil {
		err = fmt.Errorf("Close: %v", err)
		return
	}

	return
}

// Return the current size in bytes of the content and an indication of whether
// the proxied object has changed out from under us (in which case Sync will
// fail).
func (op *ObjectProxy) Stat(
	ctx context.Context) (size int64, clobbered bool, err error) {
	// Stat the object in GCS.
	req := &gcs.StatObjectRequest{Name: op.name}
	o, bucketErr := op.bucket.StatObject(ctx, req)

	// Propagate errors.
	if bucketErr != nil {
		// Propagate errors. Special case: suppress gcs.NotFoundError, treating it
		// as a zero generation below.
		if _, ok := bucketErr.(*gcs.NotFoundError); !ok {
			err = fmt.Errorf("StatObject: %v", bucketErr)
			return
		}
	}

	// Find the generation number, or zero if not found.
	var currentGen int64
	if bucketErr == nil {
		currentGen = o.Generation
	}

	// We are clobbered iff the generation doesn't match our source generation.
	clobbered = (currentGen != op.srcGeneration)

	// If we have a file, it is authoritative for our size. Otherwise our source
	// size is authoritative.
	if op.localFile != nil {
		var fi os.FileInfo
		if fi, err = op.localFile.Stat(); err != nil {
			err = fmt.Errorf("Stat: %v", err)
			return
		}

		size = fi.Size()
	} else {
		size = op.srcSize
	}

	return
}

// Make a random access read into our view of the content. May block for
// network access.
//
// Guarantees that err != nil if n < len(buf)
func (op *ObjectProxy) ReadAt(
	ctx context.Context,
	buf []byte,
	offset int64) (n int, err error) {
	// Make sure we have a local file.
	if err = op.ensureLocalFile(ctx); err != nil {
		err = fmt.Errorf("ensureLocalFile: %v", err)
		return
	}

	// Serve the read from the file.
	n, err = op.localFile.ReadAt(buf, offset)

	return
}

// Make a random access write into our view of the content. May block for
// network access. Not guaranteed to be reflected remotely until after Sync is
// called successfully.
//
// Guarantees that err != nil if n < len(buf)
func (op *ObjectProxy) WriteAt(
	ctx context.Context,
	buf []byte,
	offset int64) (n int, err error) {
	// Make sure we have a local file.
	if err = op.ensureLocalFile(ctx); err != nil {
		err = fmt.Errorf("ensureLocalFile: %v", err)
		return
	}

	op.dirty = true
	n, err = op.localFile.WriteAt(buf, offset)

	return
}

// Truncate our view of the content to the given number of bytes, extending if
// n is greater than the current size. May block for network access. Not
// guaranteed to be reflected remotely until after Sync is called successfully.
func (op *ObjectProxy) Truncate(ctx context.Context, n int64) (err error) {
	// Make sure we have a local file.
	if err = op.ensureLocalFile(ctx); err != nil {
		err = fmt.Errorf("ensureLocalFile: %v", err)
		return
	}

	// Convert to signed, which is what os.File wants.
	if n > math.MaxInt64 {
		err = fmt.Errorf("Illegal offset: %v", n)
		return
	}

	op.dirty = true
	err = op.localFile.Truncate(int64(n))

	return
}

// If the proxy is dirty due to having been written to or due to having a nil
// source, save its current contents to GCS and return a generation number for
// a generation with exactly those contents. Do so with a precondition such
// that the creation will fail if the source generation is not current. In that
// case, return an error of type *gcs.PreconditionError.
func (op *ObjectProxy) Sync(ctx context.Context) (gen int64, err error) {
	// Do we need to do anything?
	if !op.dirty {
		gen = op.srcGeneration
		return
	}

	// Seek the file to the start so that it can be used as a reader for its full
	// contents below.
	_, err = op.localFile.Seek(0, 0)
	if err != nil {
		err = fmt.Errorf("Seek: %v", err)
		return
	}

	// Write a new generation of the object with the appropriate contents, using
	// an appropriate precondition.
	signedSrcGeneration := int64(op.srcGeneration)
	req := &gcs.CreateObjectRequest{
		Attrs: storage.ObjectAttrs{
			Name: op.name,
		},
		Contents:               op.localFile,
		GenerationPrecondition: &signedSrcGeneration,
	}

	o, err := op.bucket.CreateObject(ctx, req)

	// Special case: handle precondition errors.
	if _, ok := err.(*gcs.PreconditionError); ok {
		err = &gcs.PreconditionError{
			Err: fmt.Errorf("CreateObject: %v", err),
		}

		return
	}

	// Propagate other errors more directly.
	if err != nil {
		err = fmt.Errorf("CreateObject: %v", err)
		return
	}

	// Make sure the server didn't return a zero generation number, since we use
	// that as a sentinel.
	if o.Generation == 0 {
		err = fmt.Errorf(
			"CreateObject returned invalid generation number: %v",
			o.Generation)

		return
	}

	gen = o.Generation

	// Update our state.
	op.srcGeneration = gen
	op.dirty = false

	return
}

////////////////////////////////////////////////////////////////////////
// Helpers
////////////////////////////////////////////////////////////////////////

// Set up an unlinked local temporary file for the given generation of the
// given object. Special case: generation == 0 means an empty file.
func makeLocalFile(
	ctx context.Context,
	bucket gcs.Bucket,
	name string,
	generation int64) (f *os.File, err error) {
	// Create the file.
	f, err = ioutil.TempFile("", "object_proxy")
	if err != nil {
		err = fmt.Errorf("TempFile: %v", err)
		return
	}

	// Ensure that we clean up the file if we return in error from this method.
	defer func() {
		if err != nil {
			f.Close()
			f = nil
		}
	}()

	// Unlink the file so that its inode will be garbage collected when the file
	// is closed.
	if err = os.Remove(f.Name()); err != nil {
		err = fmt.Errorf("Remove: %v", err)
		return
	}

	// Fetch the object's contents if necessary.
	if generation != 0 {
		req := &gcs.ReadObjectRequest{
			Name:       name,
			Generation: generation,
		}

		// Open for reading.
		var rc io.ReadCloser
		if rc, err = bucket.NewReader(ctx, req); err != nil {
			err = fmt.Errorf("NewReader: %v", err)
			return
		}

		// Copy to the file.
		if _, err = io.Copy(f, rc); err != nil {
			err = fmt.Errorf("Copy: %v", err)
			return
		}

		// Close.
		if err = rc.Close(); err != nil {
			err = fmt.Errorf("Close: %v", err)
			return
		}
	}

	return
}

// Ensure that op.localFile is non-nil with an authoritative view of op's
// contents.
func (op *ObjectProxy) ensureLocalFile(ctx context.Context) (err error) {
	// Is there anything to do?
	if op.localFile != nil {
		return
	}

	// Set up the file.
	f, err := makeLocalFile(ctx, op.bucket, op.name, op.srcGeneration)
	if err != nil {
		err = fmt.Errorf("makeLocalFile: %v", err)
		return
	}

	op.localFile = f
	return
}
<commit_msg>Updated NewObjectProxy.<commit_after>// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 gcsproxy

import (
	"fmt"
	"io"
	"io/ioutil"
	"math"
	"os"

	"github.com/jacobsa/gcloud/gcs"
	"golang.org/x/net/context"
	"google.golang.org/cloud/storage"
)

// A view on a particular generation of an object in GCS that allows random
// access reads and writes.
//
// Reads may involve reading from a local cache. Writes are buffered locally
// until the Sync method is called, at which time a new generation of the
// object is created.
//
// This type is not safe for concurrent access. The user must provide external
// synchronization.
type ObjectProxy struct {
	/////////////////////////
	// Dependencies
	/////////////////////////

	bucket gcs.Bucket

	/////////////////////////
	// Mutable state
	/////////////////////////

	// A record for the specific generation of the object from which our local
	// state is branched. If we have no local state, the contents of this
	// generation are exactly our contents.
	src storage.Object

	// A local temporary file containing our current contents. When non-nil, this
	// is the authority on our contents. When nil, our contents are defined by
	// 'src' above.
	localFile *os.File

	// true if localFile is present but its contents may be different from the
	// contents of our source generation. Sync needs to do work iff this is true.
	//
	// INVARIANT: If dirty, then localFile != nil
	dirty bool
}

////////////////////////////////////////////////////////////////////////
// Public interface
////////////////////////////////////////////////////////////////////////

// Create a view on the given GCS object generation.
//
// REQUIRES: o != nil
func NewObjectProxy(
	ctx context.Context,
	bucket gcs.Bucket,
	o *storage.Object) (op *ObjectProxy, err error) {
	// Set up the basic struct.
	op = &ObjectProxy{
		bucket: bucket,
		src:    *o,
	}

	return
}

// Return the name of the proxied object. This may or may not be an object that
// currently exists in the bucket.
func (op *ObjectProxy) Name() string {
	return op.name
}

// Panic if any internal invariants are violated. Careful users can call this
// at appropriate times to help debug weirdness. Consider using
// syncutil.InvariantMutex to automate the process.
func (op *ObjectProxy) CheckInvariants() {
	// INVARIANT: If srcGeneration == 0, srcSize == 0
	if op.srcGeneration == 0 && op.srcSize != 0 {
		panic("Expected zero source size.")
	}

	// INVARIANT: If srcGeneration == 0, then dirty
	if op.srcGeneration == 0 && !op.dirty {
		panic("Expected dirty.")
	}

	// INVARIANT: If dirty, then localFile != nil
	if op.dirty && op.localFile == nil {
		panic("Expected non-nil localFile.")
	}
}

// Destroy any local file caches, putting the proxy into an indeterminate
// state. Should be used before dropping the final reference to the proxy.
func (op *ObjectProxy) Destroy() (err error) {
	// Make sure that when we exit no invariants are violated.
	defer func() {
		op.srcGeneration = 1
		op.localFile = nil
		op.dirty = false
	}()

	// If we have no local file, there's nothing to do.
	if op.localFile == nil {
		return
	}

	// Close the local file.
	if err = op.localFile.Close(); err != nil {
		err = fmt.Errorf("Close: %v", err)
		return
	}

	return
}

// Return the current size in bytes of the content and an indication of whether
// the proxied object has changed out from under us (in which case Sync will
// fail).
func (op *ObjectProxy) Stat(
	ctx context.Context) (size int64, clobbered bool, err error) {
	// Stat the object in GCS.
	req := &gcs.StatObjectRequest{Name: op.name}
	o, bucketErr := op.bucket.StatObject(ctx, req)

	// Propagate errors.
	if bucketErr != nil {
		// Propagate errors. Special case: suppress gcs.NotFoundError, treating it
		// as a zero generation below.
		if _, ok := bucketErr.(*gcs.NotFoundError); !ok {
			err = fmt.Errorf("StatObject: %v", bucketErr)
			return
		}
	}

	// Find the generation number, or zero if not found.
	var currentGen int64
	if bucketErr == nil {
		currentGen = o.Generation
	}

	// We are clobbered iff the generation doesn't match our source generation.
	clobbered = (currentGen != op.srcGeneration)

	// If we have a file, it is authoritative for our size. Otherwise our source
	// size is authoritative.
	if op.localFile != nil {
		var fi os.FileInfo
		if fi, err = op.localFile.Stat(); err != nil {
			err = fmt.Errorf("Stat: %v", err)
			return
		}

		size = fi.Size()
	} else {
		size = op.srcSize
	}

	return
}

// Make a random access read into our view of the content. May block for
// network access.
//
// Guarantees that err != nil if n < len(buf)
func (op *ObjectProxy) ReadAt(
	ctx context.Context,
	buf []byte,
	offset int64) (n int, err error) {
	// Make sure we have a local file.
	if err = op.ensureLocalFile(ctx); err != nil {
		err = fmt.Errorf("ensureLocalFile: %v", err)
		return
	}

	// Serve the read from the file.
	n, err = op.localFile.ReadAt(buf, offset)

	return
}

// Make a random access write into our view of the content. May block for
// network access. Not guaranteed to be reflected remotely until after Sync is
// called successfully.
//
// Guarantees that err != nil if n < len(buf)
func (op *ObjectProxy) WriteAt(
	ctx context.Context,
	buf []byte,
	offset int64) (n int, err error) {
	// Make sure we have a local file.
	if err = op.ensureLocalFile(ctx); err != nil {
		err = fmt.Errorf("ensureLocalFile: %v", err)
		return
	}

	op.dirty = true
	n, err = op.localFile.WriteAt(buf, offset)

	return
}

// Truncate our view of the content to the given number of bytes, extending if
// n is greater than the current size. May block for network access. Not
// guaranteed to be reflected remotely until after Sync is called successfully.
func (op *ObjectProxy) Truncate(ctx context.Context, n int64) (err error) {
	// Make sure we have a local file.
	if err = op.ensureLocalFile(ctx); err != nil {
		err = fmt.Errorf("ensureLocalFile: %v", err)
		return
	}

	// Convert to signed, which is what os.File wants.
	if n > math.MaxInt64 {
		err = fmt.Errorf("Illegal offset: %v", n)
		return
	}

	op.dirty = true
	err = op.localFile.Truncate(int64(n))

	return
}

// If the proxy is dirty due to having been written to or due to having a nil
// source, save its current contents to GCS and return a generation number for
// a generation with exactly those contents. Do so with a precondition such
// that the creation will fail if the source generation is not current. In that
// case, return an error of type *gcs.PreconditionError.
func (op *ObjectProxy) Sync(ctx context.Context) (gen int64, err error) {
	// Do we need to do anything?
	if !op.dirty {
		gen = op.srcGeneration
		return
	}

	// Seek the file to the start so that it can be used as a reader for its full
	// contents below.
	_, err = op.localFile.Seek(0, 0)
	if err != nil {
		err = fmt.Errorf("Seek: %v", err)
		return
	}

	// Write a new generation of the object with the appropriate contents, using
	// an appropriate precondition.
	signedSrcGeneration := int64(op.srcGeneration)
	req := &gcs.CreateObjectRequest{
		Attrs: storage.ObjectAttrs{
			Name: op.name,
		},
		Contents:               op.localFile,
		GenerationPrecondition: &signedSrcGeneration,
	}

	o, err := op.bucket.CreateObject(ctx, req)

	// Special case: handle precondition errors.
	if _, ok := err.(*gcs.PreconditionError); ok {
		err = &gcs.PreconditionError{
			Err: fmt.Errorf("CreateObject: %v", err),
		}

		return
	}

	// Propagate other errors more directly.
	if err != nil {
		err = fmt.Errorf("CreateObject: %v", err)
		return
	}

	// Make sure the server didn't return a zero generation number, since we use
	// that as a sentinel.
	if o.Generation == 0 {
		err = fmt.Errorf(
			"CreateObject returned invalid generation number: %v",
			o.Generation)

		return
	}

	gen = o.Generation

	// Update our state.
	op.srcGeneration = gen
	op.dirty = false

	return
}

////////////////////////////////////////////////////////////////////////
// Helpers
////////////////////////////////////////////////////////////////////////

// Set up an unlinked local temporary file for the given generation of the
// given object. Special case: generation == 0 means an empty file.
func makeLocalFile(
	ctx context.Context,
	bucket gcs.Bucket,
	name string,
	generation int64) (f *os.File, err error) {
	// Create the file.
	f, err = ioutil.TempFile("", "object_proxy")
	if err != nil {
		err = fmt.Errorf("TempFile: %v", err)
		return
	}

	// Ensure that we clean up the file if we return in error from this method.
	defer func() {
		if err != nil {
			f.Close()
			f = nil
		}
	}()

	// Unlink the file so that its inode will be garbage collected when the file
	// is closed.
	if err = os.Remove(f.Name()); err != nil {
		err = fmt.Errorf("Remove: %v", err)
		return
	}

	// Fetch the object's contents if necessary.
	if generation != 0 {
		req := &gcs.ReadObjectRequest{
			Name:       name,
			Generation: generation,
		}

		// Open for reading.
		var rc io.ReadCloser
		if rc, err = bucket.NewReader(ctx, req); err != nil {
			err = fmt.Errorf("NewReader: %v", err)
			return
		}

		// Copy to the file.
		if _, err = io.Copy(f, rc); err != nil {
			err = fmt.Errorf("Copy: %v", err)
			return
		}

		// Close.
		if err = rc.Close(); err != nil {
			err = fmt.Errorf("Close: %v", err)
			return
		}
	}

	return
}

// Ensure that op.localFile is non-nil with an authoritative view of op's
// contents.
func (op *ObjectProxy) ensureLocalFile(ctx context.Context) (err error) {
	// Is there anything to do?
	if op.localFile != nil {
		return
	}

	// Set up the file.
	f, err := makeLocalFile(ctx, op.bucket, op.name, op.srcGeneration)
	if err != nil {
		err = fmt.Errorf("makeLocalFile: %v", err)
		return
	}

	op.localFile = f
	return
}
<|endoftext|>


<commit_before>package copy

import (
	"io"

	internalblobinfocache "github.com/containers/image/v5/internal/blobinfocache"
	"github.com/containers/image/v5/pkg/compression"
	compressiontypes "github.com/containers/image/v5/pkg/compression/types"
	"github.com/containers/image/v5/types"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

// bpDetectCompressionStepData contains data that the copy pipeline needs about the “detect compression” step.
type bpDetectCompressionStepData struct {
	isCompressed      bool
	format            compressiontypes.Algorithm        // Valid if isCompressed
	decompressor      compressiontypes.DecompressorFunc // Valid if isCompressed
	srcCompressorName string                            // Compressor name to possibly record in the blob info cache for the source blob.
}

// blobPipelineDetectCompressionStep updates *stream to detect its current compression format.
// srcInfo is only used for error messages.
// Returns data for other steps.
func blobPipelineDetectCompressionStep(stream *sourceStream, srcInfo types.BlobInfo) (bpDetectCompressionStepData, error) {
	// This requires us to “peek ahead” into the stream to read the initial part, which requires us to chain through another io.Reader returned by DetectCompression.
	format, decompressor, reader, err := compression.DetectCompressionFormat(stream.reader) // We could skip this in some cases, but let's keep the code path uniform
	if err != nil {
		return bpDetectCompressionStepData{}, errors.Wrapf(err, "reading blob %s", srcInfo.Digest)
	}
	stream.reader = reader

	res := bpDetectCompressionStepData{
		isCompressed: decompressor != nil,
		format:       format,
		decompressor: decompressor,
	}
	if res.isCompressed {
		res.srcCompressorName = format.Name()
	} else {
		res.srcCompressorName = internalblobinfocache.Uncompressed
	}

	if expectedFormat, known := expectedCompressionFormats[stream.info.MediaType]; known && res.isCompressed && format.Name() != expectedFormat.Name() {
		logrus.Debugf("blob %s with type %s should be compressed with %s, but compressor appears to be %s", srcInfo.Digest.String(), srcInfo.MediaType, expectedFormat.Name(), format.Name())
	}
	return res, nil
}

// bpCompressionStepData contains data that the copy pipeline needs about the compression step.
type bpCompressionStepData struct {
	operation              types.LayerCompression      // Operation to use for updating the blob metadata.
	uploadedAlgorithm      *compressiontypes.Algorithm // An algorithm parameter for the compressionOperation edits.
	uploadedAnnotations    map[string]string           // Annotations that should be set on the uploaded blob. WARNING: This is only set after the srcStream.reader is fully consumed.
	srcCompressorName      string                      // Compressor name to record in the blob info cache for the source blob.
	uploadedCompressorName string                      // Compressor name to record in the blob info cache for the uploaded blob.
	closers                []io.Closer                 // Objects to close after the upload is done, if any.
}

// blobPipelineCompressionStep updates *stream to compress and/or decompress it.
// srcInfo is only used for error messages.
// Returns data for other steps; the caller should eventually call updateCompressionEdits and perhaps recordValidatedBlobData,
// and must eventually call close.
func (c *copier) blobPipelineCompressionStep(stream *sourceStream, canModifyBlob bool,
	detected bpDetectCompressionStepData) (*bpCompressionStepData, error) {
	// WARNING: If you are adding new reasons to change the blob, update also the OptimizeDestinationImageAlreadyExists
	// short-circuit conditions
	uploadedAnnotations := map[string]string{}
	var operation types.LayerCompression
	var uploadedAlgorithm *compressiontypes.Algorithm
	srcCompressorName := detected.srcCompressorName
	var uploadedCompressorName string
	var closers []io.Closer
	succeeded := false
	defer func() {
		if !succeeded {
			for _, c := range closers {
				c.Close()
			}
		}
	}()
	if canModifyBlob && isOciEncrypted(stream.info.MediaType) {
		// PreserveOriginal due to any compression not being able to be done on an encrypted blob unless decrypted
		logrus.Debugf("Using original blob without modification for encrypted blob")
		operation = types.PreserveOriginal
		srcCompressorName = internalblobinfocache.UnknownCompression
		uploadedAlgorithm = nil
		uploadedCompressorName = internalblobinfocache.UnknownCompression
	} else if canModifyBlob && c.dest.DesiredLayerCompression() == types.Compress && !detected.isCompressed {
		logrus.Debugf("Compressing blob on the fly")
		operation = types.Compress
		pipeReader, pipeWriter := io.Pipe()
		closers = append(closers, pipeReader)

		if c.compressionFormat != nil {
			uploadedAlgorithm = c.compressionFormat
		} else {
			uploadedAlgorithm = defaultCompressionFormat
		}
		// If this fails while writing data, it will do pipeWriter.CloseWithError(); if it fails otherwise,
		// e.g. because we have exited and due to pipeReader.Close() above further writing to the pipe has failed,
		// we don’t care.
		go c.compressGoroutine(pipeWriter, stream.reader, uploadedAnnotations, *uploadedAlgorithm) // Closes pipeWriter
		stream.reader = pipeReader
		stream.info = types.BlobInfo{ // FIXME? Should we preserve more data in src.info?
			Digest: "",
			Size:   -1,
		}
		uploadedCompressorName = uploadedAlgorithm.Name()
	} else if canModifyBlob && c.dest.DesiredLayerCompression() == types.Compress && detected.isCompressed &&
		c.compressionFormat != nil && c.compressionFormat.Name() != detected.format.Name() {
		// When the blob is compressed, but the desired format is different, it first needs to be decompressed and finally
		// re-compressed using the desired format.
		logrus.Debugf("Blob will be converted")

		operation = types.PreserveOriginal
		s, err := detected.decompressor(stream.reader)
		if err != nil {
			return nil, err
		}
		closers = append(closers, s)

		pipeReader, pipeWriter := io.Pipe()
		closers = append(closers, pipeReader)

		uploadedAlgorithm = c.compressionFormat
		go c.compressGoroutine(pipeWriter, s, uploadedAnnotations, *uploadedAlgorithm) // Closes pipeWriter

		stream.reader = pipeReader
		stream.info = types.BlobInfo{ // FIXME? Should we preserve more data in src.info?
			Digest: "",
			Size:   -1,
		}
		uploadedCompressorName = uploadedAlgorithm.Name()
	} else if canModifyBlob && c.dest.DesiredLayerCompression() == types.Decompress && detected.isCompressed {
		logrus.Debugf("Blob will be decompressed")
		operation = types.Decompress
		s, err := detected.decompressor(stream.reader)
		if err != nil {
			return nil, err
		}
		closers = append(closers, s)
		stream.reader = s
		stream.info = types.BlobInfo{ // FIXME? Should we preserve more data in src.info?
			Digest: "",
			Size:   -1,
		}
		uploadedAlgorithm = nil
		uploadedCompressorName = internalblobinfocache.Uncompressed
	} else {
		// PreserveOriginal might also need to recompress the original blob if the desired compression format is different.
		logrus.Debugf("Using original blob without modification")
		operation = types.PreserveOriginal
		// Remember if the original blob was compressed, and if so how, so that if
		// LayerInfosForCopy() returned something that differs from what was in the
		// source's manifest, and UpdatedImage() needs to call UpdateLayerInfos(),
		// it will be able to correctly derive the MediaType for the copied blob.
		if detected.isCompressed {
			uploadedAlgorithm = &detected.format
		} else {
			uploadedAlgorithm = nil
		}
		uploadedCompressorName = srcCompressorName
	}
	succeeded = true
	return &bpCompressionStepData{
		operation:              operation,
		uploadedAlgorithm:      uploadedAlgorithm,
		uploadedAnnotations:    uploadedAnnotations,
		srcCompressorName:      srcCompressorName,
		uploadedCompressorName: uploadedCompressorName,
		closers:                closers,
	}, nil
}

// updateCompressionEdits sets *operation, *algorithm and updates *annotations, if necessary.
func (d *bpCompressionStepData) updateCompressionEdits(operation *types.LayerCompression, algorithm **compressiontypes.Algorithm, annotations *map[string]string) {
	*operation = d.operation
	// If we can modify the layer's blob, set the desired algorithm for it to be set in the manifest.
	*algorithm = d.uploadedAlgorithm
	if *annotations == nil {
		*annotations = map[string]string{}
	}
	for k, v := range d.uploadedAnnotations {
		(*annotations)[k] = v
	}
}

// recordValidatedBlobData updates b.blobInfoCache with data about the created uploadedInfo adnd the original srcInfo.
// This must ONLY be called if all data has been validated by OUR code, and is not comming from third parties.
func (d *bpCompressionStepData) recordValidatedDigestData(c *copier, uploadedInfo types.BlobInfo, srcInfo types.BlobInfo,
	encryptionStep *bpEncryptionStepData, decryptionStep *bpDecryptionStepData) error {
	// Don’t record any associations that involve encrypted data. This is a bit crude,
	// some blob substitutions (replacing pulls of encrypted data with local reuse of known decryption outcomes)
	// might be safe, but it’s not trivially obvious, so let’s be conservative for now.
	// This crude approach also means we don’t need to record whether a blob is encrypted
	// in the blob info cache (which would probably be necessary for any more complex logic),
	// and the simplicity is attractive.
	if !encryptionStep.encrypting && !decryptionStep.decrypting {
		// If d.operation != types.PreserveOriginal, we now have two reliable digest values:
		// srcinfo.Digest describes the pre-d.operation input, verified by digestingReader
		// uploadedInfo.Digest describes the post-d.operation output, computed by PutBlob
		// (because stream.info.Digest == "", this must have been computed afresh).
		switch d.operation {
		case types.PreserveOriginal:
			break // Do nothing, we have only one digest and we might not have even verified it.
		case types.Compress:
			c.blobInfoCache.RecordDigestUncompressedPair(uploadedInfo.Digest, srcInfo.Digest)
		case types.Decompress:
			c.blobInfoCache.RecordDigestUncompressedPair(srcInfo.Digest, uploadedInfo.Digest)
		default:
			return errors.Errorf("Internal error: Unexpected d.operation value %#v", d.operation)
		}
	}
	if d.uploadedCompressorName != "" && d.uploadedCompressorName != internalblobinfocache.UnknownCompression {
		c.blobInfoCache.RecordDigestCompressorName(uploadedInfo.Digest, d.uploadedCompressorName)
	}
	if srcInfo.Digest != "" && d.srcCompressorName != "" && d.srcCompressorName != internalblobinfocache.UnknownCompression {
		c.blobInfoCache.RecordDigestCompressorName(srcInfo.Digest, d.srcCompressorName)
	}
	return nil
}

// close closes objects that carry state throughout the compression/decompression operation.
func (d *bpCompressionStepData) close() {
	for _, c := range d.closers {
		c.Close()
	}
}

// doCompression reads all input from src and writes its compressed equivalent to dest.
func doCompression(dest io.Writer, src io.Reader, metadata map[string]string, compressionFormat compressiontypes.Algorithm, compressionLevel *int) error {
	compressor, err := compression.CompressStreamWithMetadata(dest, metadata, compressionFormat, compressionLevel)
	if err != nil {
		return err
	}

	buf := make([]byte, compressionBufferSize)

	_, err = io.CopyBuffer(compressor, src, buf) // Sets err to nil, i.e. causes dest.Close()
	if err != nil {
		compressor.Close()
		return err
	}

	return compressor.Close()
}

// compressGoroutine reads all input from src and writes its compressed equivalent to dest.
func (c *copier) compressGoroutine(dest *io.PipeWriter, src io.Reader, metadata map[string]string, compressionFormat compressiontypes.Algorithm) {
	err := errors.New("Internal error: unexpected panic in compressGoroutine")
	defer func() { // Note that this is not the same as {defer dest.CloseWithError(err)}; we need err to be evaluated lazily.
		_ = dest.CloseWithError(err) // CloseWithError(nil) is equivalent to Close(), always returns nil
	}()

	err = doCompression(dest, src, metadata, compressionFormat, c.compressionLevel)
}
<commit_msg>Return a fresh &bpCompressionStepData in each case<commit_after>package copy

import (
	"io"

	internalblobinfocache "github.com/containers/image/v5/internal/blobinfocache"
	"github.com/containers/image/v5/pkg/compression"
	compressiontypes "github.com/containers/image/v5/pkg/compression/types"
	"github.com/containers/image/v5/types"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

// bpDetectCompressionStepData contains data that the copy pipeline needs about the “detect compression” step.
type bpDetectCompressionStepData struct {
	isCompressed      bool
	format            compressiontypes.Algorithm        // Valid if isCompressed
	decompressor      compressiontypes.DecompressorFunc // Valid if isCompressed
	srcCompressorName string                            // Compressor name to possibly record in the blob info cache for the source blob.
}

// blobPipelineDetectCompressionStep updates *stream to detect its current compression format.
// srcInfo is only used for error messages.
// Returns data for other steps.
func blobPipelineDetectCompressionStep(stream *sourceStream, srcInfo types.BlobInfo) (bpDetectCompressionStepData, error) {
	// This requires us to “peek ahead” into the stream to read the initial part, which requires us to chain through another io.Reader returned by DetectCompression.
	format, decompressor, reader, err := compression.DetectCompressionFormat(stream.reader) // We could skip this in some cases, but let's keep the code path uniform
	if err != nil {
		return bpDetectCompressionStepData{}, errors.Wrapf(err, "reading blob %s", srcInfo.Digest)
	}
	stream.reader = reader

	res := bpDetectCompressionStepData{
		isCompressed: decompressor != nil,
		format:       format,
		decompressor: decompressor,
	}
	if res.isCompressed {
		res.srcCompressorName = format.Name()
	} else {
		res.srcCompressorName = internalblobinfocache.Uncompressed
	}

	if expectedFormat, known := expectedCompressionFormats[stream.info.MediaType]; known && res.isCompressed && format.Name() != expectedFormat.Name() {
		logrus.Debugf("blob %s with type %s should be compressed with %s, but compressor appears to be %s", srcInfo.Digest.String(), srcInfo.MediaType, expectedFormat.Name(), format.Name())
	}
	return res, nil
}

// bpCompressionStepData contains data that the copy pipeline needs about the compression step.
type bpCompressionStepData struct {
	operation              types.LayerCompression      // Operation to use for updating the blob metadata.
	uploadedAlgorithm      *compressiontypes.Algorithm // An algorithm parameter for the compressionOperation edits.
	uploadedAnnotations    map[string]string           // Annotations that should be set on the uploaded blob. WARNING: This is only set after the srcStream.reader is fully consumed.
	srcCompressorName      string                      // Compressor name to record in the blob info cache for the source blob.
	uploadedCompressorName string                      // Compressor name to record in the blob info cache for the uploaded blob.
	closers                []io.Closer                 // Objects to close after the upload is done, if any.
}

// blobPipelineCompressionStep updates *stream to compress and/or decompress it.
// srcInfo is only used for error messages.
// Returns data for other steps; the caller should eventually call updateCompressionEdits and perhaps recordValidatedBlobData,
// and must eventually call close.
func (c *copier) blobPipelineCompressionStep(stream *sourceStream, canModifyBlob bool,
	detected bpDetectCompressionStepData) (*bpCompressionStepData, error) {
	// WARNING: If you are adding new reasons to change the blob, update also the OptimizeDestinationImageAlreadyExists
	// short-circuit conditions
	uploadedAnnotations := map[string]string{}
	var uploadedAlgorithm *compressiontypes.Algorithm
	var closers []io.Closer
	succeeded := false
	defer func() {
		if !succeeded {
			for _, c := range closers {
				c.Close()
			}
		}
	}()
	if canModifyBlob && isOciEncrypted(stream.info.MediaType) {
		// PreserveOriginal due to any compression not being able to be done on an encrypted blob unless decrypted
		logrus.Debugf("Using original blob without modification for encrypted blob")
		succeeded = true
		return &bpCompressionStepData{
			operation:              types.PreserveOriginal,
			uploadedAlgorithm:      nil,
			srcCompressorName:      internalblobinfocache.UnknownCompression,
			uploadedCompressorName: internalblobinfocache.UnknownCompression,
			closers:                closers,
		}, nil
	} else if canModifyBlob && c.dest.DesiredLayerCompression() == types.Compress && !detected.isCompressed {
		logrus.Debugf("Compressing blob on the fly")
		pipeReader, pipeWriter := io.Pipe()
		closers = append(closers, pipeReader)

		if c.compressionFormat != nil {
			uploadedAlgorithm = c.compressionFormat
		} else {
			uploadedAlgorithm = defaultCompressionFormat
		}
		// If this fails while writing data, it will do pipeWriter.CloseWithError(); if it fails otherwise,
		// e.g. because we have exited and due to pipeReader.Close() above further writing to the pipe has failed,
		// we don’t care.
		go c.compressGoroutine(pipeWriter, stream.reader, uploadedAnnotations, *uploadedAlgorithm) // Closes pipeWriter
		stream.reader = pipeReader
		stream.info = types.BlobInfo{ // FIXME? Should we preserve more data in src.info?
			Digest: "",
			Size:   -1,
		}
		succeeded = true
		return &bpCompressionStepData{
			operation:              types.Compress,
			uploadedAlgorithm:      uploadedAlgorithm,
			srcCompressorName:      detected.srcCompressorName,
			uploadedCompressorName: uploadedAlgorithm.Name(),
			closers:                closers,
		}, nil
	} else if canModifyBlob && c.dest.DesiredLayerCompression() == types.Compress && detected.isCompressed &&
		c.compressionFormat != nil && c.compressionFormat.Name() != detected.format.Name() {
		// When the blob is compressed, but the desired format is different, it first needs to be decompressed and finally
		// re-compressed using the desired format.
		logrus.Debugf("Blob will be converted")

		s, err := detected.decompressor(stream.reader)
		if err != nil {
			return nil, err
		}
		closers = append(closers, s)

		pipeReader, pipeWriter := io.Pipe()
		closers = append(closers, pipeReader)

		go c.compressGoroutine(pipeWriter, s, uploadedAnnotations, *c.compressionFormat) // Closes pipeWriter

		stream.reader = pipeReader
		stream.info = types.BlobInfo{ // FIXME? Should we preserve more data in src.info?
			Digest: "",
			Size:   -1,
		}
		succeeded = true
		return &bpCompressionStepData{
			operation:              types.PreserveOriginal,
			uploadedAlgorithm:      c.compressionFormat,
			uploadedAnnotations:    uploadedAnnotations,
			srcCompressorName:      detected.srcCompressorName,
			uploadedCompressorName: c.compressionFormat.Name(),
			closers:                closers,
		}, nil
	} else if canModifyBlob && c.dest.DesiredLayerCompression() == types.Decompress && detected.isCompressed {
		logrus.Debugf("Blob will be decompressed")
		s, err := detected.decompressor(stream.reader)
		if err != nil {
			return nil, err
		}
		closers = append(closers, s)
		stream.reader = s
		stream.info = types.BlobInfo{ // FIXME? Should we preserve more data in src.info?
			Digest: "",
			Size:   -1,
		}
		succeeded = true
		return &bpCompressionStepData{
			operation:              types.Decompress,
			uploadedAlgorithm:      nil,
			srcCompressorName:      detected.srcCompressorName,
			uploadedCompressorName: internalblobinfocache.Uncompressed,
			closers:                closers,
		}, nil
	} else {
		// PreserveOriginal might also need to recompress the original blob if the desired compression format is different.
		logrus.Debugf("Using original blob without modification")
		// Remember if the original blob was compressed, and if so how, so that if
		// LayerInfosForCopy() returned something that differs from what was in the
		// source's manifest, and UpdatedImage() needs to call UpdateLayerInfos(),
		// it will be able to correctly derive the MediaType for the copied blob.
		if detected.isCompressed {
			uploadedAlgorithm = &detected.format
		} else {
			uploadedAlgorithm = nil
		}
		succeeded = true
		return &bpCompressionStepData{
			operation:              types.PreserveOriginal,
			uploadedAlgorithm:      uploadedAlgorithm,
			srcCompressorName:      detected.srcCompressorName,
			uploadedCompressorName: detected.srcCompressorName,
			closers:                closers,
		}, nil
	}
}

// updateCompressionEdits sets *operation, *algorithm and updates *annotations, if necessary.
func (d *bpCompressionStepData) updateCompressionEdits(operation *types.LayerCompression, algorithm **compressiontypes.Algorithm, annotations *map[string]string) {
	*operation = d.operation
	// If we can modify the layer's blob, set the desired algorithm for it to be set in the manifest.
	*algorithm = d.uploadedAlgorithm
	if *annotations == nil {
		*annotations = map[string]string{}
	}
	for k, v := range d.uploadedAnnotations {
		(*annotations)[k] = v
	}
}

// recordValidatedBlobData updates b.blobInfoCache with data about the created uploadedInfo adnd the original srcInfo.
// This must ONLY be called if all data has been validated by OUR code, and is not comming from third parties.
func (d *bpCompressionStepData) recordValidatedDigestData(c *copier, uploadedInfo types.BlobInfo, srcInfo types.BlobInfo,
	encryptionStep *bpEncryptionStepData, decryptionStep *bpDecryptionStepData) error {
	// Don’t record any associations that involve encrypted data. This is a bit crude,
	// some blob substitutions (replacing pulls of encrypted data with local reuse of known decryption outcomes)
	// might be safe, but it’s not trivially obvious, so let’s be conservative for now.
	// This crude approach also means we don’t need to record whether a blob is encrypted
	// in the blob info cache (which would probably be necessary for any more complex logic),
	// and the simplicity is attractive.
	if !encryptionStep.encrypting && !decryptionStep.decrypting {
		// If d.operation != types.PreserveOriginal, we now have two reliable digest values:
		// srcinfo.Digest describes the pre-d.operation input, verified by digestingReader
		// uploadedInfo.Digest describes the post-d.operation output, computed by PutBlob
		// (because stream.info.Digest == "", this must have been computed afresh).
		switch d.operation {
		case types.PreserveOriginal:
			break // Do nothing, we have only one digest and we might not have even verified it.
		case types.Compress:
			c.blobInfoCache.RecordDigestUncompressedPair(uploadedInfo.Digest, srcInfo.Digest)
		case types.Decompress:
			c.blobInfoCache.RecordDigestUncompressedPair(srcInfo.Digest, uploadedInfo.Digest)
		default:
			return errors.Errorf("Internal error: Unexpected d.operation value %#v", d.operation)
		}
	}
	if d.uploadedCompressorName != "" && d.uploadedCompressorName != internalblobinfocache.UnknownCompression {
		c.blobInfoCache.RecordDigestCompressorName(uploadedInfo.Digest, d.uploadedCompressorName)
	}
	if srcInfo.Digest != "" && d.srcCompressorName != "" && d.srcCompressorName != internalblobinfocache.UnknownCompression {
		c.blobInfoCache.RecordDigestCompressorName(srcInfo.Digest, d.srcCompressorName)
	}
	return nil
}

// close closes objects that carry state throughout the compression/decompression operation.
func (d *bpCompressionStepData) close() {
	for _, c := range d.closers {
		c.Close()
	}
}

// doCompression reads all input from src and writes its compressed equivalent to dest.
func doCompression(dest io.Writer, src io.Reader, metadata map[string]string, compressionFormat compressiontypes.Algorithm, compressionLevel *int) error {
	compressor, err := compression.CompressStreamWithMetadata(dest, metadata, compressionFormat, compressionLevel)
	if err != nil {
		return err
	}

	buf := make([]byte, compressionBufferSize)

	_, err = io.CopyBuffer(compressor, src, buf) // Sets err to nil, i.e. causes dest.Close()
	if err != nil {
		compressor.Close()
		return err
	}

	return compressor.Close()
}

// compressGoroutine reads all input from src and writes its compressed equivalent to dest.
func (c *copier) compressGoroutine(dest *io.PipeWriter, src io.Reader, metadata map[string]string, compressionFormat compressiontypes.Algorithm) {
	err := errors.New("Internal error: unexpected panic in compressGoroutine")
	defer func() { // Note that this is not the same as {defer dest.CloseWithError(err)}; we need err to be evaluated lazily.
		_ = dest.CloseWithError(err) // CloseWithError(nil) is equivalent to Close(), always returns nil
	}()

	err = doCompression(dest, src, metadata, compressionFormat, c.compressionLevel)
}
<|endoftext|>


<commit_before>package cli

import (
	"fmt"
	"time"

	"github.com/SeerUK/tid/pkg/state"
	"github.com/SeerUK/tid/pkg/tracking"
	"github.com/eidolon/console"
	"github.com/eidolon/console/parameters"
	"github.com/olekukonko/tablewriter"
)

const ReportDateFmt = "2006-01-02"

func ReportCommand(gateway tracking.Gateway) console.Command {
	var start time.Time
	var end time.Time

	configure := func(def *console.Definition) {
		def.AddOption(
			parameters.NewDateValue(&start),
			"-s, --start=START",
			"The start date of the report.",
		)

		def.AddOption(
			parameters.NewDateValue(&end),
			"-e, --end=END",
			"The end date of the report.",
		)
	}

	execute := func(input *console.Input, output *console.Output) error {
		hasStart := input.HasOption([]string{"s", "start"})
		hasEnd := input.HasOption([]string{"e", "end"})

		// We need to get the current date, this is a little hacky, but we need it without any time
		now, err := time.Parse(ReportDateFmt, time.Now().Format(ReportDateFmt))
		if err != nil {
			return err
		}

		if !hasStart {
			start = now
		}

		if !hasEnd {
			end = now
		}

		if start.After(end) {
			output.Println("report: The start date must be before the end date")
			return nil
		}

		keys := getDateRangeTimesheetKeys(start, end)
		sheets, err := getTimesheetsBykeys(gateway, keys)
		if err != nil {
			return err
		}

		status, err := gateway.FindOrCreateStatus()
		if err != nil {
			return err
		}

		var duration time.Duration
		var entries int

		err = forEachEntry(gateway, sheets, func(entry *tracking.Entry) {
			if status.Ref().Entry == entry.Hash() {
				entry.UpdateDuration()
				entry.Update()

				gateway.PersistEntry(entry)
			}

			duration = duration + entry.Duration()
			entries = entries + 1
		})

		if err != nil {
			return err
		}

		if entries == 0 {
			output.Println("report: No entries within the given time period")
			return nil
		}

		if start.Equal(end) {
			format := "Report for %s.\n"
			output.Printf(format, end.Format(ReportDateFmt))
			output.Println()
		} else {
			format := "Report for %s to %s.\n"
			output.Printf(format, start.Format(ReportDateFmt), end.Format(ReportDateFmt))
			output.Println()
		}

		output.Printf("Total Duration: %s\n", duration)
		output.Printf("Total Entries: %d\n", entries)
		output.Println()

		table := tablewriter.NewWriter(output.Writer)
		table.SetAlignment(tablewriter.ALIGN_LEFT)
		table.SetHeader([]string{
			"Date",
			"Hash",
			"Created",
			"Updated",
			"Note",
			"Duration",
			"Running",
		})

		dateFormat := "03:04:05PM (2006-01-02)"

		err = forEachEntry(gateway, sheets, func(entry *tracking.Entry) {
			isRunning := status.IsActive() && status.Ref().Entry == entry.Hash()

			table.Append([]string{
				entry.Timesheet(),
				entry.ShortHash(),
				entry.Created().Format(dateFormat),
				entry.Updated().Format(dateFormat),
				entry.Note(),
				entry.Duration().String(),
				fmt.Sprintf("%t", isRunning),
			})
		})

		if err != nil {
			return err
		}

		table.SetAutoMergeCells(true)
		table.SetRowLine(true)
		table.Render()

		return nil
	}

	return console.Command{
		Name:        "report",
		Description: "Display a tabular timesheet report.",
		Configure:   configure,
		Execute:     execute,
	}
}

// forEachEntry runs the given function on each entry in each timesheet in the given array of
// timesheets. This uses the database.
func forEachEntry(gw tracking.Gateway, ss []*tracking.Timesheet, fn func(*tracking.Entry)) error {
	for _, sheet := range ss {
		for _, hash := range sheet.Entries() {
			entry, err := gw.FindEntry(hash)
			if err != nil {
				return err
			}

			fn(entry)
		}
	}

	return nil
}

// getTimesheetsByKeys returns all of the timesheets that exist from an array of keys to try.
func getTimesheetsBykeys(gateway tracking.Gateway, keys []string) ([]*tracking.Timesheet, error) {
	sheets := []*tracking.Timesheet{}

	for _, key := range keys {
		sheet, err := gateway.FindTimesheet(key)
		if err != nil && err != state.ErrNilResult {
			return sheets, err
		}

		if err == state.ErrNilResult {
			continue
		}

		sheets = append(sheets, sheet)
	}

	return sheets, nil
}

// getDateRangeTimesheetKeys produces an array of keys to attempt to find timesheets within for a
// given start and end date range.
func getDateRangeTimesheetKeys(start time.Time, end time.Time) []string {
	keys := []string{}

	for current := start; !current.After(end); current = current.AddDate(0, 0, 1) {
		keys = append(keys, current.Format(tracking.KeyTimesheetDateFmt))
	}

	return keys
}
<commit_msg>No longer updating inactive but tracked entry when viewing the report.<commit_after>package cli

import (
	"fmt"
	"time"

	"github.com/SeerUK/tid/pkg/state"
	"github.com/SeerUK/tid/pkg/tracking"
	"github.com/eidolon/console"
	"github.com/eidolon/console/parameters"
	"github.com/olekukonko/tablewriter"
)

const ReportDateFmt = "2006-01-02"

func ReportCommand(gateway tracking.Gateway) console.Command {
	var start time.Time
	var end time.Time

	configure := func(def *console.Definition) {
		def.AddOption(
			parameters.NewDateValue(&start),
			"-s, --start=START",
			"The start date of the report.",
		)

		def.AddOption(
			parameters.NewDateValue(&end),
			"-e, --end=END",
			"The end date of the report.",
		)
	}

	execute := func(input *console.Input, output *console.Output) error {
		hasStart := input.HasOption([]string{"s", "start"})
		hasEnd := input.HasOption([]string{"e", "end"})

		// We need to get the current date, this is a little hacky, but we need it without any time
		now, err := time.Parse(ReportDateFmt, time.Now().Format(ReportDateFmt))
		if err != nil {
			return err
		}

		if !hasStart {
			start = now
		}

		if !hasEnd {
			end = now
		}

		if start.After(end) {
			output.Println("report: The start date must be before the end date")
			return nil
		}

		keys := getDateRangeTimesheetKeys(start, end)
		sheets, err := getTimesheetsBykeys(gateway, keys)
		if err != nil {
			return err
		}

		status, err := gateway.FindOrCreateStatus()
		if err != nil {
			return err
		}

		var duration time.Duration
		var entries int

		err = forEachEntry(gateway, sheets, func(entry *tracking.Entry) {
			if status.IsActive() && status.Ref().Entry == entry.Hash() {
				entry.UpdateDuration()
				entry.Update()

				gateway.PersistEntry(entry)
			}

			duration = duration + entry.Duration()
			entries = entries + 1
		})

		if err != nil {
			return err
		}

		if entries == 0 {
			output.Println("report: No entries within the given time period")
			return nil
		}

		if start.Equal(end) {
			format := "Report for %s.\n"
			output.Printf(format, end.Format(ReportDateFmt))
			output.Println()
		} else {
			format := "Report for %s to %s.\n"
			output.Printf(format, start.Format(ReportDateFmt), end.Format(ReportDateFmt))
			output.Println()
		}

		output.Printf("Total Duration: %s\n", duration)
		output.Printf("Total Entries: %d\n", entries)
		output.Println()

		table := tablewriter.NewWriter(output.Writer)
		table.SetAlignment(tablewriter.ALIGN_LEFT)
		table.SetHeader([]string{
			"Date",
			"Hash",
			"Created",
			"Updated",
			"Note",
			"Duration",
			"Running",
		})

		dateFormat := "03:04:05PM (2006-01-02)"

		err = forEachEntry(gateway, sheets, func(entry *tracking.Entry) {
			isRunning := status.IsActive() && status.Ref().Entry == entry.Hash()

			table.Append([]string{
				entry.Timesheet(),
				entry.ShortHash(),
				entry.Created().Format(dateFormat),
				entry.Updated().Format(dateFormat),
				entry.Note(),
				entry.Duration().String(),
				fmt.Sprintf("%t", isRunning),
			})
		})

		if err != nil {
			return err
		}

		table.SetAutoMergeCells(true)
		table.SetRowLine(true)
		table.Render()

		return nil
	}

	return console.Command{
		Name:        "report",
		Description: "Display a tabular timesheet report.",
		Configure:   configure,
		Execute:     execute,
	}
}

// forEachEntry runs the given function on each entry in each timesheet in the given array of
// timesheets. This uses the database.
func forEachEntry(gw tracking.Gateway, ss []*tracking.Timesheet, fn func(*tracking.Entry)) error {
	for _, sheet := range ss {
		for _, hash := range sheet.Entries() {
			entry, err := gw.FindEntry(hash)
			if err != nil {
				return err
			}

			fn(entry)
		}
	}

	return nil
}

// getTimesheetsByKeys returns all of the timesheets that exist from an array of keys to try.
func getTimesheetsBykeys(gateway tracking.Gateway, keys []string) ([]*tracking.Timesheet, error) {
	sheets := []*tracking.Timesheet{}

	for _, key := range keys {
		sheet, err := gateway.FindTimesheet(key)
		if err != nil && err != state.ErrNilResult {
			return sheets, err
		}

		if err == state.ErrNilResult {
			continue
		}

		sheets = append(sheets, sheet)
	}

	return sheets, nil
}

// getDateRangeTimesheetKeys produces an array of keys to attempt to find timesheets within for a
// given start and end date range.
func getDateRangeTimesheetKeys(start time.Time, end time.Time) []string {
	keys := []string{}

	for current := start; !current.After(end); current = current.AddDate(0, 0, 1) {
		keys = append(keys, current.Format(tracking.KeyTimesheetDateFmt))
	}

	return keys
}
<|endoftext|>


<commit_before>/*
Copyright 2014 Google Inc. All rights reserved.

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 cache

import (
	"fmt"
	"sync"

	"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)

// Store is a generic object storage interface. Reflector knows how to watch a server
// and update a store. A generic store is provided, which allows Reflector to be used
// as a local caching system, and an LRU store, which allows Reflector to work like a
// queue of items yet to be processed.
//
// Store makes no assumptions about stored object identity; it is the responsibility
// of a Store implementation to provide a mechanism to correctly key objects and to
// define the contract for obtaining objects by some arbitrary key type.
type Store interface {
	Add(obj interface{}) error
	Update(obj interface{}) error
	Delete(obj interface{}) error
	List() []interface{}
	Get(obj interface{}) (item interface{}, exists bool, err error)
	GetByKey(key string) (item interface{}, exists bool, err error)

	// Replace will delete the contents of the store, using instead the
	// given list. Store takes ownership of the list, you should not reference
	// it after calling this function.
	Replace([]interface{}) error
}

// KeyFunc knows how to make a key from an object. Implementations should be deterministic.
type KeyFunc func(obj interface{}) (string, error)

// MetaNamespaceKeyFunc is a convenient default KeyFunc which knows how to make
// keys for API objects which implement meta.Interface.
// The key uses the format: <namespace>/<name>
func MetaNamespaceKeyFunc(obj interface{}) (string, error) {
	meta, err := meta.Accessor(obj)
	if err != nil {
		return "", fmt.Errorf("object has no meta: %v", err)
	}
	return meta.Namespace() + "/" + meta.Name(), nil
}

type cache struct {
	lock  sync.RWMutex
	items map[string]interface{}
	// keyFunc is used to make the key for objects stored in and retrieved from items, and
	// should be deterministic.
	keyFunc KeyFunc
	// indexers maps a name to an IndexFunc
	indexers Indexers
	// indices maps a name to an Index
	indices Indices
}

// Add inserts an item into the cache.
func (c *cache) Add(obj interface{}) error {
	key, err := c.keyFunc(obj)
	if err != nil {
		return fmt.Errorf("couldn't create key for object: %v", err)
	}
	// keep a pointer to whatever could have been there previously
	c.lock.Lock()
	defer c.lock.Unlock()
	oldObject := c.items[key]
	c.items[key] = obj
	c.updateIndices(oldObject, obj)
	return nil
}

// updateIndices modifies the objects location in the managed indexes, if this is an update, you must provide an oldObj
// updateIndices must be called from a function that already has a lock on the cache
func (c *cache) updateIndices(oldObj interface{}, newObj interface{}) error {
	// if we got an old object, we need to remove it before we add it again
	if oldObj != nil {
		c.deleteFromIndices(oldObj)
	}
	key, err := c.keyFunc(newObj)
	if err != nil {
		return err
	}
	for name, indexFunc := range c.indexers {
		indexValue, err := indexFunc(newObj)
		if err != nil {
			return err
		}
		index := c.indices[name]
		if index == nil {
			index = Index{}
			c.indices[name] = index
		}
		set := index[indexValue]
		if set == nil {
			set = util.StringSet{}
			index[indexValue] = set
		}
		set.Insert(key)
	}
	return nil
}

// deleteFromIndices removes the object from each of the managed indexes
// it is intended to be called from a function that already has a lock on the cache
func (c *cache) deleteFromIndices(obj interface{}) error {
	key, err := c.keyFunc(obj)
	if err != nil {
		return err
	}
	for name, indexFunc := range c.indexers {
		indexValue, err := indexFunc(obj)
		if err != nil {
			return err
		}
		index := c.indices[name]
		if index != nil {
			set := index[indexValue]
			if set != nil {
				set.Delete(key)
			}
		}
	}
	return nil
}

// Update sets an item in the cache to its updated state.
func (c *cache) Update(obj interface{}) error {
	key, err := c.keyFunc(obj)
	if err != nil {
		return fmt.Errorf("couldn't create key for object: %v", err)
	}
	c.lock.Lock()
	defer c.lock.Unlock()
	oldObject := c.items[key]
	c.items[key] = obj
	c.updateIndices(oldObject, obj)
	return nil
}

// Delete removes an item from the cache.
func (c *cache) Delete(obj interface{}) error {
	key, err := c.keyFunc(obj)
	if err != nil {
		return fmt.Errorf("couldn't create key for object: %v", err)
	}
	c.lock.Lock()
	defer c.lock.Unlock()
	delete(c.items, key)
	c.deleteFromIndices(obj)
	return nil
}

// List returns a list of all the items.
// List is completely threadsafe as long as you treat all items as immutable.
func (c *cache) List() []interface{} {
	c.lock.RLock()
	defer c.lock.RUnlock()
	list := make([]interface{}, 0, len(c.items))
	for _, item := range c.items {
		list = append(list, item)
	}
	return list
}

// Index returns a list of items that match on the index function
// Index is thread-safe so long as you treat all items as immutable
func (c *cache) Index(indexName string, obj interface{}) ([]interface{}, error) {
	c.lock.RLock()
	defer c.lock.RUnlock()

	indexFunc := c.indexers[indexName]
	if indexFunc == nil {
		return nil, fmt.Errorf("Index with name %s does not exist", indexName)
	}

	indexKey, err := indexFunc(obj)
	if err != nil {
		return nil, err
	}
	index := c.indices[indexName]
	set := index[indexKey]
	list := make([]interface{}, 0, set.Len())
	for _, key := range set.List() {
		list = append(list, c.items[key])
	}
	return list, nil
}

// Get returns the requested item, or sets exists=false.
// Get is completely threadsafe as long as you treat all items as immutable.
func (c *cache) Get(obj interface{}) (item interface{}, exists bool, err error) {
	key, _ := c.keyFunc(obj)
	if err != nil {
		return nil, false, fmt.Errorf("couldn't create key for object: %v", err)
	}
	return c.GetByKey(key)
}

// GetByKey returns the request item, or exists=false.
// GetByKey is completely threadsafe as long as you treat all items as immutable.
func (c *cache) GetByKey(key string) (item interface{}, exists bool, err error) {
	c.lock.RLock()
	defer c.lock.RUnlock()
	item, exists = c.items[key]
	return item, exists, nil
}

// Replace will delete the contents of 'c', using instead the given list.
// 'c' takes ownership of the list, you should not reference the list again
// after calling this function.
func (c *cache) Replace(list []interface{}) error {
	items := map[string]interface{}{}
	for _, item := range list {
		key, err := c.keyFunc(item)
		if err != nil {
			return fmt.Errorf("couldn't create key for object: %v", err)
		}
		items[key] = item
	}

	c.lock.Lock()
	defer c.lock.Unlock()
	c.items = items

	// rebuild any index
	c.indices = Indices{}
	for _, item := range c.items {
		c.updateIndices(nil, item)
	}

	return nil
}

// NewStore returns a Store implemented simply with a map and a lock.
func NewStore(keyFunc KeyFunc) Store {
	return &cache{items: map[string]interface{}{}, keyFunc: keyFunc, indexers: Indexers{}, indices: Indices{}}
}

// NewIndexer returns an Indexer implemented simply with a map and a lock.
func NewIndexer(keyFunc KeyFunc, indexers Indexers) Indexer {
	return &cache{items: map[string]interface{}{}, keyFunc: keyFunc, indexers: indexers, indices: Indices{}}
}
<commit_msg>If an object has no namespace, do not add a leading slash<commit_after>/*
Copyright 2014 Google Inc. All rights reserved.

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 cache

import (
	"fmt"
	"sync"

	"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)

// Store is a generic object storage interface. Reflector knows how to watch a server
// and update a store. A generic store is provided, which allows Reflector to be used
// as a local caching system, and an LRU store, which allows Reflector to work like a
// queue of items yet to be processed.
//
// Store makes no assumptions about stored object identity; it is the responsibility
// of a Store implementation to provide a mechanism to correctly key objects and to
// define the contract for obtaining objects by some arbitrary key type.
type Store interface {
	Add(obj interface{}) error
	Update(obj interface{}) error
	Delete(obj interface{}) error
	List() []interface{}
	Get(obj interface{}) (item interface{}, exists bool, err error)
	GetByKey(key string) (item interface{}, exists bool, err error)

	// Replace will delete the contents of the store, using instead the
	// given list. Store takes ownership of the list, you should not reference
	// it after calling this function.
	Replace([]interface{}) error
}

// KeyFunc knows how to make a key from an object. Implementations should be deterministic.
type KeyFunc func(obj interface{}) (string, error)

// MetaNamespaceKeyFunc is a convenient default KeyFunc which knows how to make
// keys for API objects which implement meta.Interface.
// The key uses the format: <namespace>/<name>
func MetaNamespaceKeyFunc(obj interface{}) (string, error) {
	meta, err := meta.Accessor(obj)
	if err != nil {
		return "", fmt.Errorf("object has no meta: %v", err)
	}
	if len(meta.Namespace()) > 0 {
		return meta.Namespace() + "/" + meta.Name(), nil
	}
	return meta.Name(), nil
}

type cache struct {
	lock  sync.RWMutex
	items map[string]interface{}
	// keyFunc is used to make the key for objects stored in and retrieved from items, and
	// should be deterministic.
	keyFunc KeyFunc
	// indexers maps a name to an IndexFunc
	indexers Indexers
	// indices maps a name to an Index
	indices Indices
}

// Add inserts an item into the cache.
func (c *cache) Add(obj interface{}) error {
	key, err := c.keyFunc(obj)
	if err != nil {
		return fmt.Errorf("couldn't create key for object: %v", err)
	}
	// keep a pointer to whatever could have been there previously
	c.lock.Lock()
	defer c.lock.Unlock()
	oldObject := c.items[key]
	c.items[key] = obj
	c.updateIndices(oldObject, obj)
	return nil
}

// updateIndices modifies the objects location in the managed indexes, if this is an update, you must provide an oldObj
// updateIndices must be called from a function that already has a lock on the cache
func (c *cache) updateIndices(oldObj interface{}, newObj interface{}) error {
	// if we got an old object, we need to remove it before we add it again
	if oldObj != nil {
		c.deleteFromIndices(oldObj)
	}
	key, err := c.keyFunc(newObj)
	if err != nil {
		return err
	}
	for name, indexFunc := range c.indexers {
		indexValue, err := indexFunc(newObj)
		if err != nil {
			return err
		}
		index := c.indices[name]
		if index == nil {
			index = Index{}
			c.indices[name] = index
		}
		set := index[indexValue]
		if set == nil {
			set = util.StringSet{}
			index[indexValue] = set
		}
		set.Insert(key)
	}
	return nil
}

// deleteFromIndices removes the object from each of the managed indexes
// it is intended to be called from a function that already has a lock on the cache
func (c *cache) deleteFromIndices(obj interface{}) error {
	key, err := c.keyFunc(obj)
	if err != nil {
		return err
	}
	for name, indexFunc := range c.indexers {
		indexValue, err := indexFunc(obj)
		if err != nil {
			return err
		}
		index := c.indices[name]
		if index != nil {
			set := index[indexValue]
			if set != nil {
				set.Delete(key)
			}
		}
	}
	return nil
}

// Update sets an item in the cache to its updated state.
func (c *cache) Update(obj interface{}) error {
	key, err := c.keyFunc(obj)
	if err != nil {
		return fmt.Errorf("couldn't create key for object: %v", err)
	}
	c.lock.Lock()
	defer c.lock.Unlock()
	oldObject := c.items[key]
	c.items[key] = obj
	c.updateIndices(oldObject, obj)
	return nil
}

// Delete removes an item from the cache.
func (c *cache) Delete(obj interface{}) error {
	key, err := c.keyFunc(obj)
	if err != nil {
		return fmt.Errorf("couldn't create key for object: %v", err)
	}
	c.lock.Lock()
	defer c.lock.Unlock()
	delete(c.items, key)
	c.deleteFromIndices(obj)
	return nil
}

// List returns a list of all the items.
// List is completely threadsafe as long as you treat all items as immutable.
func (c *cache) List() []interface{} {
	c.lock.RLock()
	defer c.lock.RUnlock()
	list := make([]interface{}, 0, len(c.items))
	for _, item := range c.items {
		list = append(list, item)
	}
	return list
}

// Index returns a list of items that match on the index function
// Index is thread-safe so long as you treat all items as immutable
func (c *cache) Index(indexName string, obj interface{}) ([]interface{}, error) {
	c.lock.RLock()
	defer c.lock.RUnlock()

	indexFunc := c.indexers[indexName]
	if indexFunc == nil {
		return nil, fmt.Errorf("Index with name %s does not exist", indexName)
	}

	indexKey, err := indexFunc(obj)
	if err != nil {
		return nil, err
	}
	index := c.indices[indexName]
	set := index[indexKey]
	list := make([]interface{}, 0, set.Len())
	for _, key := range set.List() {
		list = append(list, c.items[key])
	}
	return list, nil
}

// Get returns the requested item, or sets exists=false.
// Get is completely threadsafe as long as you treat all items as immutable.
func (c *cache) Get(obj interface{}) (item interface{}, exists bool, err error) {
	key, _ := c.keyFunc(obj)
	if err != nil {
		return nil, false, fmt.Errorf("couldn't create key for object: %v", err)
	}
	return c.GetByKey(key)
}

// GetByKey returns the request item, or exists=false.
// GetByKey is completely threadsafe as long as you treat all items as immutable.
func (c *cache) GetByKey(key string) (item interface{}, exists bool, err error) {
	c.lock.RLock()
	defer c.lock.RUnlock()
	item, exists = c.items[key]
	return item, exists, nil
}

// Replace will delete the contents of 'c', using instead the given list.
// 'c' takes ownership of the list, you should not reference the list again
// after calling this function.
func (c *cache) Replace(list []interface{}) error {
	items := map[string]interface{}{}
	for _, item := range list {
		key, err := c.keyFunc(item)
		if err != nil {
			return fmt.Errorf("couldn't create key for object: %v", err)
		}
		items[key] = item
	}

	c.lock.Lock()
	defer c.lock.Unlock()
	c.items = items

	// rebuild any index
	c.indices = Indices{}
	for _, item := range c.items {
		c.updateIndices(nil, item)
	}

	return nil
}

// NewStore returns a Store implemented simply with a map and a lock.
func NewStore(keyFunc KeyFunc) Store {
	return &cache{items: map[string]interface{}{}, keyFunc: keyFunc, indexers: Indexers{}, indices: Indices{}}
}

// NewIndexer returns an Indexer implemented simply with a map and a lock.
func NewIndexer(keyFunc KeyFunc, indexers Indexers) Indexer {
	return &cache{items: map[string]interface{}{}, keyFunc: keyFunc, indexers: indexers, indices: Indices{}}
}
<|endoftext|>


<commit_before>package dsdk

import (
	"context"
	"fmt"
	_path "path"
	"strconv"

	greq "github.com/levigross/grequests"
)

type AppInstance struct {
	AccessControlMode       string                  `json:"access_control_mode,omitempty" mapstructure:"access_control_mode"`
	AdminState              string                  `json:"admin_state,omitempty" mapstructure:"admin_state"`
	AppTemplate             *AppInstanceAppTemplate `json:"app_template,omitempty" mapstructure:"app_template"`
	Causes                  []string                `json:"causes,omitempty" mapstructure:"causes"`
	CloneSrc                *AppInstance            `json:"clone_src,omitempty" mapstructure:"clone_src"`
	CreateMode              string                  `json:"create_mode,omitempty" mapstructure:"create_mode"`
	DeploymentState         string                  `json:"deployment_state,omitempty" mapstructure:"deployment_state"`
	Descr                   string                  `json:"descr,omitempty" mapstructure:"descr"`
	Health                  string                  `json:"health,omitempty" mapstructure:"health"`
	Id                      string                  `json:"id,omitempty" mapstructure:"id"`
	Name                    string                  `json:"name,omitempty" mapstructure:"name"`
	OpState                 string                  `json:"op_state,omitempty" mapstructure:"op_state"`
	Path                    string                  `json:"path,omitempty" mapstructure:"path"`
	RemoteRestorePercentage int                     `json:"remote_restore_percentage,omitempty" mapstructure:"remote_restore_percentage"`
	RemoteRestoreProgress   string                  `json:"remote_restore_progress,omitempty" mapstructure:"remote_restore_progress"`
	RepairPriority          string                  `json:"repair_priority,omitempty" mapstructure:"repair_priority"`
	RestorePoint            string                  `json:"restore_point,omitempty" mapstructure:"restore_point"`
	RestoreProgress         string                  `json:"restore_progress,omitempty" mapstructure:"restore_progress"`
	SnapshotPolicies        []*SnapshotPolicy       `json:"snapshot_policies,omitempty" mapstructure:"snapshot_policies"`
	Snapshots               []*Snapshot             `json:"snapshots,omitempty" mapstructure:"snapshots"`
	StorageInstances        []*StorageInstance      `json:"storage_instances,omitempty" mapstructure:"storage_instances"`
	StoragePool             []*StoragePool          `json:"storage_pool,omitempty" mapstructure:"storage_pool"`
	TemplateOverride        map[string]interface{}  `json:"template_override,omitempty" mapstructure:"template_override"`
	Uuid                    string                  `json:"uuid,omitempty" mapstructure:"uuid"`
	StorageInstancesEp      *StorageInstances       `json:"-"`
	SnapshotsEp             *Snapshots              `json:"-"`
}

func RegisterAppInstanceEndpoints(a *AppInstance) {
	a.StorageInstancesEp = newStorageInstances(a.Path)
	a.SnapshotsEp = newSnapshots(a.Path)
	for _, si := range a.StorageInstances {
		RegisterStorageInstanceEndpoints(si)
	}
}

type AppInstances struct {
	Path string
}

type AppInstancesCreateRequest struct {
	Ctxt             context.Context         `json:"-"`
	AppTemplate      *AppInstanceAppTemplate `json:"app_template,omitempty" mapstructure:"app_template"`
	CloneSnapshotSrc *Snapshot               `json:"clone_snapshot_src,omitempty" mapstructure:"clone_snapshot_src"`
	CloneVolumeSrc   *Volume                 `json:"clone_volume_src,omitempty" mapstructure:"clone_volume_src"`
	CloneSrc         *AppInstance            `json:"clone_src,omitempty" mapstructure:"clone_src"`
	CreateMode       string                  `json:"create_mode,omitempty" mapstructure:"create_mode"`
	Descr            string                  `json:"descr,omitempty" mapstructure:"descr"`
	Name             string                  `json:"name,omitempty" mapstructure:"name"`
	RepairPriority   string                  `json:"repair_priority,omitempty" mapstructure:"repair_priority"`
	SnapshotPolicies []*SnapshotPolicy       `json:"snapshot_policies,omitempty" mapstructure:"snapshot_policies"`
	StorageInstances []*StorageInstance      `json:"storage_instances,omitempty" mapstructure:"storage_instances"`
	StoragePool      []*StoragePool          `json:"storage_pool,omitempty" mapstructure:"storage_pool"`
	TemplateOverride map[string]interface{}  `json:"template_override,omitempty" mapstructure:"template_override"`
}

func newAppInstances(path string) *AppInstances {
	return &AppInstances{
		Path: _path.Join(path, "app_instances"),
	}
}

func (e *AppInstances) Create(ro *AppInstancesCreateRequest) (*AppInstance, *ApiErrorResponse, error) {
	gro := &greq.RequestOptions{JSON: ro}
	rs, apierr, err := GetConn(ro.Ctxt).Post(ro.Ctxt, e.Path, gro)
	Log().Debugf("App Instance create request sent to go-sdk with following data, %s", string(ro))
	if apierr != nil {
		return nil, apierr, err
	}
	if err != nil {
		return nil, nil, err
	}
	resp := &AppInstance{}
	if err = FillStruct(rs.Data, resp); err != nil {
		return nil, nil, err
	}
	RegisterAppInstanceEndpoints(resp)
	return resp, nil, nil
}

type AppInstancesListRequest struct {
	Ctxt   context.Context `json:"-"`
	Params ListParams      `json:"params,omitempty"`
}

func (e *AppInstances) List(ro *AppInstancesListRequest) ([]*AppInstance, *ApiErrorResponse, error) {
	gro := &greq.RequestOptions{
		JSON:   ro,
		Params: ro.Params.ToMap()}
	rs, apierr, err := GetConn(ro.Ctxt).GetList(ro.Ctxt, e.Path, gro)
	if apierr != nil {
		return nil, apierr, err
	}
	if err != nil {
		return nil, nil, err
	}
	resp := []*AppInstance{}
	for _, data := range rs.Data {
		elem := &AppInstance{}
		adata := data.(map[string]interface{})
		if err = FillStruct(adata, elem); err != nil {
			return nil, nil, err
		}
		RegisterAppInstanceEndpoints(elem)
		resp = append(resp, elem)
	}
	return resp, nil, nil
}

type AppInstancesGetRequest struct {
	Ctxt context.Context `json:"-"`
	Id   string          `json:"-"`
}

func (e *AppInstances) Get(ro *AppInstancesGetRequest) (*AppInstance, *ApiErrorResponse, error) {
	gro := &greq.RequestOptions{JSON: ro}
	rs, apierr, err := GetConn(ro.Ctxt).Get(ro.Ctxt, _path.Join(e.Path, ro.Id), gro)
	if apierr != nil {
		return nil, apierr, err
	}
	if err != nil {
		return nil, nil, err
	}
	resp := &AppInstance{}
	if err = FillStruct(rs.Data, resp); err != nil {
		return nil, nil, err
	}
	RegisterAppInstanceEndpoints(resp)
	return resp, nil, nil
}

type AppInstanceSetRequest struct {
	Ctxt               context.Context    `json:"-"`
	AdminState         string             `json:"admin_state,omitempty" mapstructure:"admin_state"`
	Descr              string             `json:"descr,omitempty" mapstructure:"descr"`
	Force              bool               `json:"force,omitempty" mapstructure:"force"`
	Name               string             `json:"name,omitempty" mapstructure:"name"`
	Provisioned        string             `json:"provisioned,omitempty" mapstructure:"provisioned"`
	RemoteProvider     string             `json:"remote_provider,omitempty" mapstructure:"remote_provider"`
	RemoteRestorePoint string             `json:"remote_restore_point,omitempty" mapstructure:"remote_restore_point"`
	RepairPriority     string             `json:"repair_priority,omitempty" mapstructure:"repair_priority"`
	RestorePoint       string             `json:"restore_point,omitempty" mapstructure:"restore_point"`
	SnapshotPolicies   []*SnapshotPolicy  `json:"snapshot_policies,omitempty" mapstructure:"snapshot_policies"`
	StorageInstances   []*StorageInstance `json:"storage_instances,omitempty" mapstructure:"storage_instances"`
	StoragePool        []*StoragePool     `json:"storage_pool,omitempty" mapstructure:"storage_pool"`
}

func (e *AppInstance) Set(ro *AppInstanceSetRequest) (*AppInstance, *ApiErrorResponse, error) {
	gro := &greq.RequestOptions{JSON: ro}
	rs, apierr, err := GetConn(ro.Ctxt).Put(ro.Ctxt, e.Path, gro)
	if apierr != nil {
		return nil, apierr, err
	}
	if err != nil {
		return nil, nil, err
	}
	resp := &AppInstance{}
	if err = FillStruct(rs.Data, resp); err != nil {
		return nil, nil, err
	}
	RegisterAppInstanceEndpoints(resp)
	return resp, nil, nil
}

type AppInstanceDeleteRequest struct {
	Ctxt  context.Context `json:"-"`
	Force bool            `json:"force,omitempty" mapstructure:"force"`
}

func (e *AppInstance) Delete(ro *AppInstanceDeleteRequest) (*AppInstance, *ApiErrorResponse, error) {
	rs, apierr, err := GetConn(ro.Ctxt).Delete(ro.Ctxt, e.Path, nil)
	if apierr != nil {
		return nil, apierr, err
	}
	if err != nil {
		return nil, nil, err
	}
	resp := &AppInstance{}
	if err = FillStruct(rs.Data, resp); err != nil {
		return nil, nil, err
	}
	RegisterAppInstanceEndpoints(resp)
	return resp, nil, nil
}

type AppInstanceAppTemplate struct {
	Path           string `json:"path,omitempty" mapstructure:"path"`
	ResolvedPath   string `json:"resolved_path,omitempty" mapstructure:"resolved_path"`
	ResolvedTenant string `json:"resolved_tenant,omitempty" mapstructure:"resolved_tenant"`
}

type AppInstanceMetadata map[string]string

type AppInstanceMetadataGetRequest struct {
	Ctxt context.Context `json:"-"`
}

func (e *AppInstance) GetMetadata(ro *AppInstanceMetadataGetRequest) (*AppInstanceMetadata, *ApiErrorResponse, error) {
	gro := &greq.RequestOptions{JSON: ro}
	rs, apierr, err := GetConn(ro.Ctxt).Get(ro.Ctxt, _path.Join(e.Path, "metadata"), gro)
	if apierr != nil {
		return nil, apierr, err
	}
	if err != nil {
		return nil, nil, err
	}
	return stringifyResults(rs), nil, nil
}

type AppInstanceMetadataSetRequest struct {
	Ctxt     context.Context `json:"-"`
	Metadata map[string]string
}

func (e *AppInstance) SetMetadata(ro *AppInstanceMetadataSetRequest) (*AppInstanceMetadata, *ApiErrorResponse, error) {
	gro := &greq.RequestOptions{JSON: ro.Metadata}
	rs, apierr, err := GetConn(ro.Ctxt).Put(ro.Ctxt, _path.Join(e.Path, "metadata"), gro)
	if apierr != nil {
		return nil, apierr, err
	}
	if err != nil {
		return nil, nil, err
	}
	return stringifyResults(rs), nil, nil
}

func stringifyResults(rs *ApiOuter) *AppInstanceMetadata {
	resp := &AppInstanceMetadata{}
	for k, v := range rs.Data {
		var nv string
		switch t := v.(type) {
		case string:
			nv = v.(string)
		case bool:
			nv = strconv.FormatBool(v.(bool))
		case int:
			nv = strconv.FormatInt(int64(v.(int)), 10)
		default:
			panic(fmt.Sprintf("Don't know this, what do?: %s", t))
		}

		(*resp)[k] = nv
	}
	return resp
}

type AppInstanceReloadRequest struct {
	Ctxt context.Context `json:"-"`
}

func (e *AppInstance) Reload(ro *AppInstanceReloadRequest) (*AppInstance, *ApiErrorResponse, error) {
	gro := &greq.RequestOptions{JSON: ro}
	rs, apierr, err := GetConn(ro.Ctxt).Get(ro.Ctxt, e.Path, gro)
	if apierr != nil {
		return nil, apierr, err
	}
	if err != nil {
		return nil, nil, err
	}
	resp := &AppInstance{}
	if err = FillStruct(rs.Data, resp); err != nil {
		return nil, nil, err
	}
	RegisterAppInstanceEndpoints(resp)
	return resp, nil, nil
}
<commit_msg>Update app_instances.go<commit_after>package dsdk

import (
	"context"
	"fmt"
	_path "path"
	"strconv"

	greq "github.com/levigross/grequests"
)

type AppInstance struct {
	AccessControlMode       string                  `json:"access_control_mode,omitempty" mapstructure:"access_control_mode"`
	AdminState              string                  `json:"admin_state,omitempty" mapstructure:"admin_state"`
	AppTemplate             *AppInstanceAppTemplate `json:"app_template,omitempty" mapstructure:"app_template"`
	Causes                  []string                `json:"causes,omitempty" mapstructure:"causes"`
	CloneSrc                *AppInstance            `json:"clone_src,omitempty" mapstructure:"clone_src"`
	CreateMode              string                  `json:"create_mode,omitempty" mapstructure:"create_mode"`
	DeploymentState         string                  `json:"deployment_state,omitempty" mapstructure:"deployment_state"`
	Descr                   string                  `json:"descr,omitempty" mapstructure:"descr"`
	Health                  string                  `json:"health,omitempty" mapstructure:"health"`
	Id                      string                  `json:"id,omitempty" mapstructure:"id"`
	Name                    string                  `json:"name,omitempty" mapstructure:"name"`
	OpState                 string                  `json:"op_state,omitempty" mapstructure:"op_state"`
	Path                    string                  `json:"path,omitempty" mapstructure:"path"`
	RemoteRestorePercentage int                     `json:"remote_restore_percentage,omitempty" mapstructure:"remote_restore_percentage"`
	RemoteRestoreProgress   string                  `json:"remote_restore_progress,omitempty" mapstructure:"remote_restore_progress"`
	RepairPriority          string                  `json:"repair_priority,omitempty" mapstructure:"repair_priority"`
	RestorePoint            string                  `json:"restore_point,omitempty" mapstructure:"restore_point"`
	RestoreProgress         string                  `json:"restore_progress,omitempty" mapstructure:"restore_progress"`
	SnapshotPolicies        []*SnapshotPolicy       `json:"snapshot_policies,omitempty" mapstructure:"snapshot_policies"`
	Snapshots               []*Snapshot             `json:"snapshots,omitempty" mapstructure:"snapshots"`
	StorageInstances        []*StorageInstance      `json:"storage_instances,omitempty" mapstructure:"storage_instances"`
	StoragePool             []*StoragePool          `json:"storage_pool,omitempty" mapstructure:"storage_pool"`
	TemplateOverride        map[string]interface{}  `json:"template_override,omitempty" mapstructure:"template_override"`
	Uuid                    string                  `json:"uuid,omitempty" mapstructure:"uuid"`
	StorageInstancesEp      *StorageInstances       `json:"-"`
	SnapshotsEp             *Snapshots              `json:"-"`
}

func RegisterAppInstanceEndpoints(a *AppInstance) {
	a.StorageInstancesEp = newStorageInstances(a.Path)
	a.SnapshotsEp = newSnapshots(a.Path)
	for _, si := range a.StorageInstances {
		RegisterStorageInstanceEndpoints(si)
	}
}

type AppInstances struct {
	Path string
}

type AppInstancesCreateRequest struct {
	Ctxt             context.Context         `json:"-"`
	AppTemplate      *AppInstanceAppTemplate `json:"app_template,omitempty" mapstructure:"app_template"`
	CloneSnapshotSrc *Snapshot               `json:"clone_snapshot_src,omitempty" mapstructure:"clone_snapshot_src"`
	CloneVolumeSrc   *Volume                 `json:"clone_volume_src,omitempty" mapstructure:"clone_volume_src"`
	CloneSrc         *AppInstance            `json:"clone_src,omitempty" mapstructure:"clone_src"`
	CreateMode       string                  `json:"create_mode,omitempty" mapstructure:"create_mode"`
	Descr            string                  `json:"descr,omitempty" mapstructure:"descr"`
	Name             string                  `json:"name,omitempty" mapstructure:"name"`
	RepairPriority   string                  `json:"repair_priority,omitempty" mapstructure:"repair_priority"`
	SnapshotPolicies []*SnapshotPolicy       `json:"snapshot_policies,omitempty" mapstructure:"snapshot_policies"`
	StorageInstances []*StorageInstance      `json:"storage_instances,omitempty" mapstructure:"storage_instances"`
	StoragePool      []*StoragePool          `json:"storage_pool,omitempty" mapstructure:"storage_pool"`
	TemplateOverride map[string]interface{}  `json:"template_override,omitempty" mapstructure:"template_override"`
}

func newAppInstances(path string) *AppInstances {
	return &AppInstances{
		Path: _path.Join(path, "app_instances"),
	}
}

func (e *AppInstances) Create(ro *AppInstancesCreateRequest) (*AppInstance, *ApiErrorResponse, error) {
	gro := &greq.RequestOptions{JSON: ro}
	rs, apierr, err := GetConn(ro.Ctxt).Post(ro.Ctxt, e.Path, gro)
	Log().Debugf("App Instance create request sent to go-sdk with following data, %s", ro)
	if apierr != nil {
		return nil, apierr, err
	}
	if err != nil {
		return nil, nil, err
	}
	resp := &AppInstance{}
	if err = FillStruct(rs.Data, resp); err != nil {
		return nil, nil, err
	}
	RegisterAppInstanceEndpoints(resp)
	return resp, nil, nil
}

type AppInstancesListRequest struct {
	Ctxt   context.Context `json:"-"`
	Params ListParams      `json:"params,omitempty"`
}

func (e *AppInstances) List(ro *AppInstancesListRequest) ([]*AppInstance, *ApiErrorResponse, error) {
	gro := &greq.RequestOptions{
		JSON:   ro,
		Params: ro.Params.ToMap()}
	rs, apierr, err := GetConn(ro.Ctxt).GetList(ro.Ctxt, e.Path, gro)
	if apierr != nil {
		return nil, apierr, err
	}
	if err != nil {
		return nil, nil, err
	}
	resp := []*AppInstance{}
	for _, data := range rs.Data {
		elem := &AppInstance{}
		adata := data.(map[string]interface{})
		if err = FillStruct(adata, elem); err != nil {
			return nil, nil, err
		}
		RegisterAppInstanceEndpoints(elem)
		resp = append(resp, elem)
	}
	return resp, nil, nil
}

type AppInstancesGetRequest struct {
	Ctxt context.Context `json:"-"`
	Id   string          `json:"-"`
}

func (e *AppInstances) Get(ro *AppInstancesGetRequest) (*AppInstance, *ApiErrorResponse, error) {
	gro := &greq.RequestOptions{JSON: ro}
	rs, apierr, err := GetConn(ro.Ctxt).Get(ro.Ctxt, _path.Join(e.Path, ro.Id), gro)
	if apierr != nil {
		return nil, apierr, err
	}
	if err != nil {
		return nil, nil, err
	}
	resp := &AppInstance{}
	if err = FillStruct(rs.Data, resp); err != nil {
		return nil, nil, err
	}
	RegisterAppInstanceEndpoints(resp)
	return resp, nil, nil
}

type AppInstanceSetRequest struct {
	Ctxt               context.Context    `json:"-"`
	AdminState         string             `json:"admin_state,omitempty" mapstructure:"admin_state"`
	Descr              string             `json:"descr,omitempty" mapstructure:"descr"`
	Force              bool               `json:"force,omitempty" mapstructure:"force"`
	Name               string             `json:"name,omitempty" mapstructure:"name"`
	Provisioned        string             `json:"provisioned,omitempty" mapstructure:"provisioned"`
	RemoteProvider     string             `json:"remote_provider,omitempty" mapstructure:"remote_provider"`
	RemoteRestorePoint string             `json:"remote_restore_point,omitempty" mapstructure:"remote_restore_point"`
	RepairPriority     string             `json:"repair_priority,omitempty" mapstructure:"repair_priority"`
	RestorePoint       string             `json:"restore_point,omitempty" mapstructure:"restore_point"`
	SnapshotPolicies   []*SnapshotPolicy  `json:"snapshot_policies,omitempty" mapstructure:"snapshot_policies"`
	StorageInstances   []*StorageInstance `json:"storage_instances,omitempty" mapstructure:"storage_instances"`
	StoragePool        []*StoragePool     `json:"storage_pool,omitempty" mapstructure:"storage_pool"`
}

func (e *AppInstance) Set(ro *AppInstanceSetRequest) (*AppInstance, *ApiErrorResponse, error) {
	gro := &greq.RequestOptions{JSON: ro}
	rs, apierr, err := GetConn(ro.Ctxt).Put(ro.Ctxt, e.Path, gro)
	if apierr != nil {
		return nil, apierr, err
	}
	if err != nil {
		return nil, nil, err
	}
	resp := &AppInstance{}
	if err = FillStruct(rs.Data, resp); err != nil {
		return nil, nil, err
	}
	RegisterAppInstanceEndpoints(resp)
	return resp, nil, nil
}

type AppInstanceDeleteRequest struct {
	Ctxt  context.Context `json:"-"`
	Force bool            `json:"force,omitempty" mapstructure:"force"`
}

func (e *AppInstance) Delete(ro *AppInstanceDeleteRequest) (*AppInstance, *ApiErrorResponse, error) {
	rs, apierr, err := GetConn(ro.Ctxt).Delete(ro.Ctxt, e.Path, nil)
	if apierr != nil {
		return nil, apierr, err
	}
	if err != nil {
		return nil, nil, err
	}
	resp := &AppInstance{}
	if err = FillStruct(rs.Data, resp); err != nil {
		return nil, nil, err
	}
	RegisterAppInstanceEndpoints(resp)
	return resp, nil, nil
}

type AppInstanceAppTemplate struct {
	Path           string `json:"path,omitempty" mapstructure:"path"`
	ResolvedPath   string `json:"resolved_path,omitempty" mapstructure:"resolved_path"`
	ResolvedTenant string `json:"resolved_tenant,omitempty" mapstructure:"resolved_tenant"`
}

type AppInstanceMetadata map[string]string

type AppInstanceMetadataGetRequest struct {
	Ctxt context.Context `json:"-"`
}

func (e *AppInstance) GetMetadata(ro *AppInstanceMetadataGetRequest) (*AppInstanceMetadata, *ApiErrorResponse, error) {
	gro := &greq.RequestOptions{JSON: ro}
	rs, apierr, err := GetConn(ro.Ctxt).Get(ro.Ctxt, _path.Join(e.Path, "metadata"), gro)
	if apierr != nil {
		return nil, apierr, err
	}
	if err != nil {
		return nil, nil, err
	}
	return stringifyResults(rs), nil, nil
}

type AppInstanceMetadataSetRequest struct {
	Ctxt     context.Context `json:"-"`
	Metadata map[string]string
}

func (e *AppInstance) SetMetadata(ro *AppInstanceMetadataSetRequest) (*AppInstanceMetadata, *ApiErrorResponse, error) {
	gro := &greq.RequestOptions{JSON: ro.Metadata}
	rs, apierr, err := GetConn(ro.Ctxt).Put(ro.Ctxt, _path.Join(e.Path, "metadata"), gro)
	if apierr != nil {
		return nil, apierr, err
	}
	if err != nil {
		return nil, nil, err
	}
	return stringifyResults(rs), nil, nil
}

func stringifyResults(rs *ApiOuter) *AppInstanceMetadata {
	resp := &AppInstanceMetadata{}
	for k, v := range rs.Data {
		var nv string
		switch t := v.(type) {
		case string:
			nv = v.(string)
		case bool:
			nv = strconv.FormatBool(v.(bool))
		case int:
			nv = strconv.FormatInt(int64(v.(int)), 10)
		default:
			panic(fmt.Sprintf("Don't know this, what do?: %s", t))
		}

		(*resp)[k] = nv
	}
	return resp
}

type AppInstanceReloadRequest struct {
	Ctxt context.Context `json:"-"`
}

func (e *AppInstance) Reload(ro *AppInstanceReloadRequest) (*AppInstance, *ApiErrorResponse, error) {
	gro := &greq.RequestOptions{JSON: ro}
	rs, apierr, err := GetConn(ro.Ctxt).Get(ro.Ctxt, e.Path, gro)
	if apierr != nil {
		return nil, apierr, err
	}
	if err != nil {
		return nil, nil, err
	}
	resp := &AppInstance{}
	if err = FillStruct(rs.Data, resp); err != nil {
		return nil, nil, err
	}
	RegisterAppInstanceEndpoints(resp)
	return resp, nil, nil
}
<|endoftext|>