resource/file/fetch/fetch.go
// Copyright © 2016 Asteris, LLC
//
// 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 fetch
import (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"fmt"
"hash"
"io"
"net/url"
"os"
"github.com/asteris-llc/converge/resource"
"github.com/hashicorp/go-getter"
"github.com/pkg/errors"
"golang.org/x/net/context"
)
// Hash type for Fetch
type Hash string
const (
// HashMD5 indicates hash type md5
HashMD5 Hash = "md5"
// HashSHA1 indicates hash type sha1
HashSHA1 Hash = "sha1"
// HashSHA256 indicates hash type sha256
HashSHA256 Hash = "sha256"
// HashSHA512 indicates hash type sha512
HashSHA512 Hash = "sha512"
)
// Fetch gets a file and makes it available on disk
type Fetch struct {
// location of the file to fetch
Source string `export:"source"`
// destination for the fetched file
Destination string `export:"destination"`
// hash function used to generate the checksum hash; value is available for
// lookup if set in the hcl
HashType string `export:"hash_type"`
// the checksum hash; value is available for lookup if set in the hcl
Hash string `export:"hash"`
// whether the file will be fetched if it already exists
Force bool `export:"force"`
// whether the fetched file will be unarchived
Unarchive bool
hasApplied bool
}
// response struct
// contains response (resource.TaskStatus and error) from Check and Apply
type response struct {
status resource.TaskStatus
err error
}
// Check if changes are needed for Fetch
func (f *Fetch) Check(ctx context.Context, r resource.Renderer) (resource.TaskStatus, error) {
ch := make(chan response, 1)
go func(ctx context.Context, r resource.Renderer) {
status, err := f.checkWithContext(ctx, r)
ch <- response{status, err}
}(ctx, r)
select {
case <-ctx.Done():
return nil, ctx.Err()
case check := <-ch:
return check.status, check.err
}
}
// Apply changes for Fetch
func (f *Fetch) Apply(ctx context.Context) (resource.TaskStatus, error) {
ch := make(chan response, 1)
go func(ctx context.Context) {
status, err := f.applyWithContext(ctx)
ch <- response{status, err}
}(ctx)
select {
case <-ctx.Done():
return nil, ctx.Err()
case apply := <-ch:
return apply.status, apply.err
}
}
// checkWithContext implements Check for Fetch
func (f *Fetch) checkWithContext(context.Context, resource.Renderer) (resource.TaskStatus, error) {
var (
hsh hash.Hash
err error
status = resource.NewStatus()
)
if f.hasApplied {
return status, nil
}
if f.Hash != "" {
hsh, err = f.getHash()
if err != nil {
status.RaiseLevel(resource.StatusCantChange)
return status, errors.Wrap(err, "will not attempt file fetch")
}
}
status, err = f.DiffFile(status, hsh)
return status, err
}
// applyWithContext implements Apply for Fetch
func (f *Fetch) applyWithContext(context.Context) (resource.TaskStatus, error) {
var (
hsh hash.Hash
err error
status = resource.NewStatus()
checksum = ""
mode = getter.ClientModeFile
)
if f.Hash != "" {
hsh, err = f.getHash()
if err != nil {
status.RaiseLevel(resource.StatusCantChange)
return status, errors.Wrap(err, "will not attempt file fetch")
}
}
stat, err := f.DiffFile(status, hsh)
if err != nil {
return stat, err
} else if !resource.AnyChanges(stat.Differences) {
return status, nil
}
u, err := url.Parse(f.Source)
if err != nil {
status.RaiseLevel(resource.StatusFatal)
return status, errors.Wrap(err, "could not parse source")
}
values := u.Query()
if f.Unarchive == false {
values.Set("archive", "false")
} else {
mode = getter.ClientModeAny
}
if f.Hash != "" {
checksum = fmt.Sprintf("checksum=%s:%s", f.HashType, f.Hash)
}
source := u.String() + "?" + values.Encode() + checksum
pwd, err := os.Getwd()
if err != nil {
status.RaiseLevel(resource.StatusFatal)
return status, errors.Wrap(err, "failed to get working directory")
}
client := &getter.Client{
Src: source,
Dst: f.Destination,
Pwd: pwd,
Mode: mode,
}
if err := client.Get(); err != nil {
status.RaiseLevel(resource.StatusFatal)
return status, errors.Wrap(err, "failed to fetch")
}
status.AddMessage("fetched successfully")
f.hasApplied = true
return status, nil
}
// DiffFile evaluates the differences of the file to be fetched and the current
// state of the system
func (f *Fetch) DiffFile(status *resource.Status, hsh hash.Hash) (*resource.Status, error) {
// the destination should be a file if fetching without an unarchive
// if unarchiving, the destination should be a directory
stat, err := os.Stat(f.Destination)
if err == nil {
if stat.IsDir() {
if f.Unarchive == false {
status.RaiseLevel(resource.StatusCantChange)
return status, fmt.Errorf("invalid destination %q, cannot be directory", f.Destination)
}
status.RaiseLevel(resource.StatusWillChange)
status.AddDifference("destination", "<absent>", f.Destination, "")
return status, nil
}
if f.Unarchive {
status.RaiseLevel(resource.StatusCantChange)
return status, fmt.Errorf("invalid destination %q for unarchiving, must be directory", f.Destination)
}
} else if os.IsNotExist(err) {
status.RaiseLevel(resource.StatusWillChange)
status.AddDifference("destination", "<absent>", f.Destination, "")
return status, nil
}
// file exists, evaluate what needs to change
if hsh != nil {
actual, err := f.getChecksum(hsh)
if err != nil {
status.RaiseLevel(resource.StatusFatal)
return status, err
}
// evaluate the checksums
if actual == f.Hash {
status.AddMessage("file exists")
} else if f.Force {
status.AddDifference("checksum", actual, f.Hash, "")
status.AddMessage("checksum mismatch")
} else {
status.AddMessage("checksum mismatch, use the \"force\" option to replace")
status.RaiseLevel(resource.StatusCantChange)
return status, errors.New("will not attempt fetch: checksum mismatch")
}
} else {
if f.Force {
status.AddDifference("destination", "<force fetch>", f.Destination, "")
status.AddMessage("file exists, will fetch due to \"force\"")
} else {
status.AddMessage("file exists")
}
}
status.RaiseLevelForDiffs()
return status, nil
}
// getHash returns a new hash based on the f.HashType
func (f *Fetch) getHash() (hash.Hash, error) {
switch f.HashType {
case string(HashMD5):
return md5.New(), nil
case string(HashSHA1):
return sha1.New(), nil
case string(HashSHA256):
return sha256.New(), nil
case string(HashSHA512):
return sha512.New(), nil
default:
return nil, fmt.Errorf("unsupported hashType %q", f.HashType)
}
}
// checksum obtains the checksum of the destination
func (f *Fetch) getChecksum(hsh hash.Hash) (string, error) {
file, err := os.Open(f.Destination)
if err != nil {
return "", errors.Wrap(err, "failed to open file for checksum")
}
defer file.Close()
if _, err := io.Copy(hsh, file); err != nil {
return "", errors.Wrap(err, "failed to hash")
}
return hex.EncodeToString(hsh.Sum(nil)), nil
}