mirror of https://github.com/pulumi/pulumi.git
427 lines
12 KiB
Go
427 lines
12 KiB
Go
// Copyright 2016-2018, Pulumi Corporation.
|
|
//
|
|
// 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 asset
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/sig"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/httputil"
|
|
)
|
|
|
|
// Asset is a serialized asset reference. It is a union: thus, at most one of its fields will be non-nil. Several helper
|
|
// routines exist as members in order to easily interact with the assets referenced by an instance of this type.
|
|
type Asset struct {
|
|
// Sig is the unique asset type signature (see properties.go).
|
|
Sig string `json:"4dabf18193072939515e22adb298388d" yaml:"4dabf18193072939515e22adb298388d"`
|
|
// Hash is the SHA256 hash of the asset's contents.
|
|
Hash string `json:"hash,omitempty" yaml:"hash,omitempty"`
|
|
// Text is set to a non-empty value for textual assets.
|
|
Text string `json:"text,omitempty" yaml:"text,omitempty"`
|
|
// Path will contain a non-empty path to the file on the current filesystem for file assets.
|
|
Path string `json:"path,omitempty" yaml:"path,omitempty"`
|
|
// URI will contain a non-empty URI (file://, http://, https://, or custom) for URI-backed assets.
|
|
URI string `json:"uri,omitempty" yaml:"uri,omitempty"`
|
|
}
|
|
|
|
const (
|
|
AssetSig = sig.AssetSig
|
|
AssetHashProperty = "hash" // the dynamic property for an asset's hash.
|
|
AssetTextProperty = "text" // the dynamic property for an asset's text.
|
|
AssetPathProperty = "path" // the dynamic property for an asset's path.
|
|
AssetURIProperty = "uri" // the dynamic property for an asset's URI.
|
|
)
|
|
|
|
// FromText produces a new asset and its corresponding SHA256 hash from the given text.
|
|
func FromText(text string) (*Asset, error) {
|
|
a := &Asset{Sig: AssetSig, Text: text}
|
|
// Special case the empty string otherwise EnsureHash will fail.
|
|
if text == "" {
|
|
a.Hash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
}
|
|
err := a.EnsureHash()
|
|
return a, err
|
|
}
|
|
|
|
// FromPath produces a new asset and its corresponding SHA256 hash from the given filesystem path.
|
|
func FromPath(path string) (*Asset, error) {
|
|
a := &Asset{Sig: AssetSig, Path: path}
|
|
err := a.EnsureHash()
|
|
return a, err
|
|
}
|
|
|
|
// FromPathWithWD produces a new asset and its corresponding SHA256 hash from the given filesystem path.
|
|
func FromPathWithWD(path string, wd string) (*Asset, error) {
|
|
a := &Asset{Sig: AssetSig, Path: path}
|
|
err := a.EnsureHashWithWD(wd)
|
|
return a, err
|
|
}
|
|
|
|
// FromURI produces a new asset and its corresponding SHA256 hash from the given network URI.
|
|
func FromURI(uri string) (*Asset, error) {
|
|
a := &Asset{Sig: AssetSig, URI: uri}
|
|
err := a.EnsureHash()
|
|
return a, err
|
|
}
|
|
|
|
func (a *Asset) IsText() bool {
|
|
if a.IsPath() || a.IsURI() {
|
|
return false
|
|
}
|
|
if a.Text != "" {
|
|
return true
|
|
}
|
|
// We can't easily tell the difference between an Asset that really has the empty string as its text and one that
|
|
// has no text at all. If we have a hash we can check if that's the "zero hash" and if so then we know the text is
|
|
// just empty. If the hash does not equal the empty hash then we know this is a _placeholder_ asset where the text is
|
|
// just currently not known. If we don't have a hash then we can't tell the difference and assume it's just empty.
|
|
if a.Hash == "" || a.Hash == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
func (a *Asset) IsPath() bool { return a.Path != "" }
|
|
func (a *Asset) IsURI() bool { return a.URI != "" }
|
|
|
|
func (a *Asset) GetText() (string, bool) {
|
|
if a.IsText() {
|
|
return a.Text, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func (a *Asset) GetPath() (string, bool) {
|
|
if a.IsPath() {
|
|
return a.Path, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func (a *Asset) GetURI() (string, bool) {
|
|
if a.IsURI() {
|
|
return a.URI, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// GetURIURL returns the underlying URI as a parsed URL, provided it is one. If there was an error parsing the URI, it
|
|
// will be returned as a non-nil error object.
|
|
func (a *Asset) GetURIURL() (*url.URL, bool, error) {
|
|
if uri, isuri := a.GetURI(); isuri {
|
|
url, err := url.Parse(uri)
|
|
if err != nil {
|
|
return nil, true, err
|
|
}
|
|
return url, true, nil
|
|
}
|
|
return nil, false, nil
|
|
}
|
|
|
|
// Equals returns true if a is value-equal to other. In this case, value equality is determined only by the hash: even
|
|
// if the contents of two assets come from different sources, they are treated as equal if their hashes match.
|
|
// Similarly, if the contents of two assets come from the same source but the assets have different hashes, the assets
|
|
// are not equal.
|
|
func (a *Asset) Equals(other *Asset) bool {
|
|
if a == nil {
|
|
return other == nil
|
|
} else if other == nil {
|
|
return false
|
|
}
|
|
|
|
// If we can't get a hash for both assets, treat them as differing.
|
|
if err := a.EnsureHash(); err != nil {
|
|
return false
|
|
}
|
|
if err := other.EnsureHash(); err != nil {
|
|
return false
|
|
}
|
|
return a.Hash == other.Hash
|
|
}
|
|
|
|
// Serialize returns a weakly typed map that contains the right signature for serialization purposes.
|
|
func (a *Asset) Serialize() map[string]interface{} {
|
|
result := map[string]interface{}{
|
|
sig.Key: AssetSig,
|
|
}
|
|
if a.Hash != "" {
|
|
result[AssetHashProperty] = a.Hash
|
|
}
|
|
if a.Text != "" {
|
|
result[AssetTextProperty] = a.Text
|
|
}
|
|
if a.Path != "" {
|
|
result[AssetPathProperty] = a.Path
|
|
}
|
|
if a.URI != "" {
|
|
result[AssetURIProperty] = a.URI
|
|
}
|
|
return result
|
|
}
|
|
|
|
// DeserializeAsset checks to see if the map contains an asset, using its signature, and if so deserializes it.
|
|
func Deserialize(obj map[string]interface{}) (*Asset, bool, error) {
|
|
// If not an asset, return false immediately.
|
|
if obj[sig.Key] != AssetSig {
|
|
return &Asset{}, false, nil
|
|
}
|
|
|
|
// Else, deserialize the possible fields.
|
|
var hash string
|
|
if v, has := obj[AssetHashProperty]; has {
|
|
h, ok := v.(string)
|
|
if !ok {
|
|
return &Asset{}, false, fmt.Errorf("unexpected asset hash of type %T", v)
|
|
}
|
|
hash = h
|
|
}
|
|
var text string
|
|
if v, has := obj[AssetTextProperty]; has {
|
|
t, ok := v.(string)
|
|
if !ok {
|
|
return &Asset{}, false, fmt.Errorf("unexpected asset text of type %T", v)
|
|
}
|
|
text = t
|
|
}
|
|
var path string
|
|
if v, has := obj[AssetPathProperty]; has {
|
|
p, ok := v.(string)
|
|
if !ok {
|
|
return &Asset{}, false, fmt.Errorf("unexpected asset path of type %T", v)
|
|
}
|
|
path = p
|
|
}
|
|
var uri string
|
|
if v, has := obj[AssetURIProperty]; has {
|
|
u, ok := v.(string)
|
|
if !ok {
|
|
return &Asset{}, false, fmt.Errorf("unexpected asset URI of type %T", v)
|
|
}
|
|
uri = u
|
|
}
|
|
|
|
return &Asset{Sig: AssetSig, Hash: hash, Text: text, Path: path, URI: uri}, true, nil
|
|
}
|
|
|
|
// HasContents indicates whether or not an asset's contents can be read.
|
|
func (a *Asset) HasContents() bool {
|
|
return a.IsText() || a.IsPath() || a.IsURI()
|
|
}
|
|
|
|
// Bytes returns the contents of the asset as a byte slice.
|
|
func (a *Asset) Bytes() ([]byte, error) {
|
|
// If this is a text asset, just return its bytes directly.
|
|
if text, istext := a.GetText(); istext {
|
|
return []byte(text), nil
|
|
}
|
|
|
|
blob, err := a.Read()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return io.ReadAll(blob)
|
|
}
|
|
|
|
// Read begins reading an asset.
|
|
func (a *Asset) Read() (*Blob, error) {
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return a.ReadWithWD(wd)
|
|
}
|
|
|
|
// ReadWithWD begins reading an asset.
|
|
func (a *Asset) ReadWithWD(wd string) (*Blob, error) {
|
|
if a.IsText() {
|
|
return a.readText()
|
|
} else if a.IsPath() {
|
|
return a.readPath(wd)
|
|
} else if a.IsURI() {
|
|
return a.readURI()
|
|
}
|
|
return nil, errors.New("unrecognized asset type")
|
|
}
|
|
|
|
func (a *Asset) readText() (*Blob, error) {
|
|
text, istext := a.GetText()
|
|
contract.Assertf(istext, "Expected a text-based asset")
|
|
return NewByteBlob([]byte(text)), nil
|
|
}
|
|
|
|
func (a *Asset) readPath(wd string) (*Blob, error) {
|
|
path, ispath := a.GetPath()
|
|
contract.Assertf(ispath, "Expected a path-based asset")
|
|
|
|
if !filepath.IsAbs(path) {
|
|
path = filepath.Join(wd, path)
|
|
}
|
|
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open asset file '%v': %w", path, err)
|
|
}
|
|
|
|
// Do a quick check to make sure it's a file, so we can fail gracefully if someone passes a directory.
|
|
info, err := file.Stat()
|
|
if err != nil {
|
|
contract.IgnoreClose(file)
|
|
return nil, fmt.Errorf("failed to stat asset file '%v': %w", path, err)
|
|
}
|
|
if info.IsDir() {
|
|
contract.IgnoreClose(file)
|
|
return nil, fmt.Errorf("asset path '%v' is a directory; try using an archive", path)
|
|
}
|
|
|
|
blob := &Blob{
|
|
rd: file,
|
|
sz: info.Size(),
|
|
}
|
|
return blob, nil
|
|
}
|
|
|
|
func (a *Asset) readURI() (*Blob, error) {
|
|
url, isURL, err := a.GetURIURL()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
contract.Assertf(isURL, "Expected a URI-based asset")
|
|
switch s := url.Scheme; s {
|
|
case "http", "https":
|
|
resp, err := httputil.GetWithRetry(url.String(), http.DefaultClient)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewReadCloserBlob(resp.Body)
|
|
case "file":
|
|
contract.Assertf(url.User == nil, "file:// URIs cannot have a user: %v", url)
|
|
contract.Assertf(url.RawQuery == "", "file:// URIs cannot have a query string: %v", url)
|
|
contract.Assertf(url.Fragment == "", "file:// URIs cannot have a fragment: %v", url)
|
|
if url.Host != "" && url.Host != "localhost" {
|
|
return nil, fmt.Errorf("file:// host '%v' not supported (only localhost)", url.Host)
|
|
}
|
|
f, err := os.Open(url.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewFileBlob(f)
|
|
default:
|
|
return nil, fmt.Errorf("Unrecognized or unsupported URI scheme: %v", s)
|
|
}
|
|
}
|
|
|
|
// EnsureHash computes the SHA256 hash of the asset's contents and stores it on the object.
|
|
func (a *Asset) EnsureHash() error {
|
|
if a.Hash == "" {
|
|
blob, err := a.Read()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer contract.IgnoreClose(blob)
|
|
|
|
hash := sha256.New()
|
|
n, err := io.Copy(hash, blob)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n != blob.Size() {
|
|
return fmt.Errorf("incorrect blob size: expected %v, got %v", blob.Size(), n)
|
|
}
|
|
a.Hash = hex.EncodeToString(hash.Sum(nil))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EnsureHash computes the SHA256 hash of the asset's contents and stores it on the object.
|
|
func (a *Asset) EnsureHashWithWD(wd string) error {
|
|
if a.Hash == "" {
|
|
blob, err := a.ReadWithWD(wd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer contract.IgnoreClose(blob)
|
|
|
|
hash := sha256.New()
|
|
n, err := io.Copy(hash, blob)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n != blob.Size() {
|
|
return fmt.Errorf("incorrect blob size: expected %v, got %v", blob.Size(), n)
|
|
}
|
|
a.Hash = hex.EncodeToString(hash.Sum(nil))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Blob is a blob that implements ReadCloser and offers Len functionality.
|
|
type Blob struct {
|
|
rd io.ReadCloser // an underlying reader.
|
|
sz int64 // the size of the blob.
|
|
}
|
|
|
|
func (blob *Blob) Close() error { return blob.rd.Close() }
|
|
func (blob *Blob) Read(p []byte) (int, error) { return blob.rd.Read(p) }
|
|
func (blob *Blob) Size() int64 { return blob.sz }
|
|
|
|
// NewByteBlob creates a new byte blob.
|
|
func NewByteBlob(data []byte) *Blob {
|
|
return &Blob{
|
|
rd: io.NopCloser(bytes.NewReader(data)),
|
|
sz: int64(len(data)),
|
|
}
|
|
}
|
|
|
|
// NewFileBlob creates a new asset blob whose size is known thanks to stat.
|
|
func NewFileBlob(f *os.File) (*Blob, error) {
|
|
stat, err := f.Stat()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Blob{
|
|
rd: f,
|
|
sz: stat.Size(),
|
|
}, nil
|
|
}
|
|
|
|
// NewReadCloserBlob turn any old ReadCloser into an Blob, usually by making a copy.
|
|
func NewReadCloserBlob(r io.ReadCloser) (*Blob, error) {
|
|
if f, isf := r.(*os.File); isf {
|
|
// If it's a file, we can "fast path" the asset creation without making a copy.
|
|
return NewFileBlob(f)
|
|
}
|
|
// Otherwise, read it all in, and create a blob out of that.
|
|
defer contract.IgnoreClose(r)
|
|
data, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewByteBlob(data), nil
|
|
}
|
|
|
|
func NewRawBlob(r io.ReadCloser, size int64) *Blob {
|
|
return &Blob{rd: r, sz: size}
|
|
}
|