github.com/hashicorp/packer@v1.14.3/packer/plugin-getter/release/getter.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package release
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"log"
    12  	"net/http"
    13  	"path/filepath"
    14  	"strings"
    15  
    16  	plugingetter "github.com/hashicorp/packer/packer/plugin-getter"
    17  	gh "github.com/hashicorp/packer/packer/plugin-getter/github"
    18  )
    19  
    20  const officialReleaseURL = "https://releases.hashicorp.com/"
    21  
    22  type Getter struct {
    23  	APIMajor   string
    24  	APIMinor   string
    25  	HttpClient *http.Client
    26  	Name       string
    27  }
    28  
    29  var _ plugingetter.Getter = &Getter{}
    30  
    31  func transformZipStream() func(in io.ReadCloser) (io.ReadCloser, error) {
    32  	return func(in io.ReadCloser) (io.ReadCloser, error) {
    33  		defer in.Close()
    34  		buf := new(bytes.Buffer)
    35  		_, err := io.Copy(buf, in)
    36  		if err != nil {
    37  			panic(err)
    38  		}
    39  		return io.NopCloser(buf), nil
    40  	}
    41  }
    42  
    43  // transformReleasesVersionStream get a stream from github tags and transforms it into
    44  // something Packer wants, namely a json list of Release.
    45  func transformReleasesVersionStream(in io.ReadCloser) (io.ReadCloser, error) {
    46  	if in == nil {
    47  		return nil, fmt.Errorf("transformReleasesVersionStream got nil body")
    48  	}
    49  	defer in.Close()
    50  	dec := json.NewDecoder(in)
    51  
    52  	var m gh.PluginMetadata
    53  	if err := dec.Decode(&m); err != nil {
    54  		return nil, err
    55  	}
    56  
    57  	var out []plugingetter.Release
    58  	for _, m := range m.Versions {
    59  		out = append(out, plugingetter.Release{
    60  			Version: "v" + m.Version,
    61  		})
    62  	}
    63  
    64  	buf := &bytes.Buffer{}
    65  	if err := json.NewEncoder(buf).Encode(out); err != nil {
    66  		return nil, err
    67  	}
    68  
    69  	return io.NopCloser(buf), nil
    70  }
    71  
    72  func (g *Getter) Get(what string, opts plugingetter.GetOptions) (io.ReadCloser, error) {
    73  	log.Printf("[TRACE] Getting %s of %s plugin from %s", what, opts.PluginRequirement.Identifier, g.Name)
    74  	// The gitHub plugin we are using because we are not changing the plugin source string, if we decide to change that,
    75  	// then we need to write this method for release getter as well, but that will change the packer init and install command as well
    76  	ghURI, err := gh.NewGithubPlugin(opts.PluginRequirement.Identifier)
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  
    81  	if g.HttpClient == nil {
    82  		g.HttpClient = &http.Client{}
    83  	}
    84  
    85  	var req *http.Request
    86  	transform := transformZipStream()
    87  
    88  	switch what {
    89  	case "releases":
    90  		// https://releases.hashicorp.com/packer-plugin-docker/index.json
    91  		url := filepath.ToSlash(officialReleaseURL + ghURI.PluginType() + "/index.json")
    92  		req, err = http.NewRequest("GET", url, nil)
    93  		transform = transformReleasesVersionStream
    94  	case "sha256":
    95  		// https://releases.hashicorp.com/packer-plugin-docker/8.0.0/packer-plugin-docker_1.1.1_SHA256SUMS
    96  		url := filepath.ToSlash(officialReleaseURL + ghURI.PluginType() + "/" + opts.VersionString() + "/" + ghURI.PluginType() + "_" + opts.VersionString() + "_SHA256SUMS")
    97  		transform = gh.TransformChecksumStream()
    98  		req, err = http.NewRequest("GET", url, nil)
    99  	case "meta":
   100  		// https://releases.hashicorp.com/packer-plugin-docker/8.0.0/packer-plugin-docker_1.1.1_manifest.json
   101  		url := filepath.ToSlash(officialReleaseURL + ghURI.PluginType() + "/" + opts.VersionString() + "/" + ghURI.PluginType() + "_" + opts.VersionString() + "_manifest.json")
   102  		req, err = http.NewRequest("GET", url, nil)
   103  	case "zip":
   104  		// https://releases.hashicorp.com/packer-plugin-docker/1.1.1/packer-plugin-docker_1.1.1_darwin_arm64.zip
   105  		url := filepath.ToSlash(officialReleaseURL + ghURI.PluginType() + "/" + opts.VersionString() + "/" + opts.ExpectedZipFilename())
   106  		req, err = http.NewRequest("GET", url, nil)
   107  	default:
   108  		return nil, fmt.Errorf("%q not implemented", what)
   109  	}
   110  
   111  	if err != nil {
   112  		log.Printf("[ERROR] http-getter: error creating request for %q: %s", what, err)
   113  		return nil, err
   114  	}
   115  
   116  	resp, err := g.HttpClient.Do(req)
   117  	if err != nil || resp.StatusCode >= 400 {
   118  		log.Printf("[ERROR] Got error while trying getting data from releases.hashicorp.com, %v", err)
   119  		return nil, plugingetter.HTTPFailure
   120  	}
   121  
   122  	defer func(Body io.ReadCloser) {
   123  		err = Body.Close()
   124  		if err != nil {
   125  			log.Printf("[ERROR] http-getter: error closing response body: %s", err)
   126  		}
   127  	}(resp.Body)
   128  
   129  	return transform(resp.Body)
   130  }
   131  
   132  // Init method : a file inside will look like so:
   133  //
   134  //	packer-plugin-comment_0.2.12_freebsd_amd64.zip
   135  func (g *Getter) Init(req *plugingetter.Requirement, entry *plugingetter.ChecksumFileEntry) error {
   136  	filename := entry.Filename
   137  	//remove the test line below where hardcoded prefix being used
   138  	res := strings.TrimPrefix(filename, req.FilenamePrefix())
   139  	// res now looks like v0.2.12_freebsd_amd64.zip
   140  
   141  	entry.Ext = filepath.Ext(res)
   142  
   143  	res = strings.TrimSuffix(res, entry.Ext)
   144  	// res now looks like 0.2.12_freebsd_amd64
   145  
   146  	parts := strings.Split(res, "_")
   147  	// ["0.2.12", "freebsd", "amd64"]
   148  	if len(parts) < 3 {
   149  		return fmt.Errorf("malformed filename expected %s{version}_{os}_{arch}", req.FilenamePrefix())
   150  	}
   151  
   152  	entry.BinVersion, entry.Os, entry.Arch = parts[0], parts[1], parts[2]
   153  	entry.BinVersion = strings.TrimPrefix(entry.BinVersion, "v")
   154  
   155  	return nil
   156  }
   157  
   158  func (g *Getter) Validate(opt plugingetter.GetOptions, expectedVersion string, installOpts plugingetter.BinaryInstallationOptions, entry *plugingetter.ChecksumFileEntry) error {
   159  
   160  	if entry.BinVersion != expectedVersion {
   161  		return fmt.Errorf("wrong version: %s does not match expected %s", entry.BinVersion, expectedVersion)
   162  	}
   163  	if entry.Os != installOpts.OS || entry.Arch != installOpts.ARCH {
   164  		return fmt.Errorf("wrong system, expected %s_%s got %s_%s", installOpts.OS, installOpts.ARCH, entry.Os, entry.Arch)
   165  	}
   166  
   167  	manifest, err := g.Get("meta", opt)
   168  	if err != nil {
   169  		return err
   170  	}
   171  
   172  	var data plugingetter.ManifestMeta
   173  	body, err := io.ReadAll(manifest)
   174  	if err != nil {
   175  		log.Printf("Failed to unmarshal manifest json: %s", err)
   176  		return err
   177  	}
   178  
   179  	err = json.Unmarshal(body, &data)
   180  	if err != nil {
   181  		log.Printf("Failed to unmarshal manifest json: %s", err)
   182  		return err
   183  	}
   184  
   185  	err = installOpts.CheckProtocolVersion("x" + data.Metadata.ProtocolVersion)
   186  	if err != nil {
   187  		return err
   188  	}
   189  
   190  	g.APIMajor = strings.Split(data.Metadata.ProtocolVersion, ".")[0]
   191  	g.APIMinor = strings.Split(data.Metadata.ProtocolVersion, ".")[1]
   192  
   193  	return nil
   194  }
   195  
   196  func (g *Getter) ExpectedFileName(pr *plugingetter.Requirement, version string, entry *plugingetter.ChecksumFileEntry, zipFileName string) string {
   197  	pluginSourceParts := strings.Split(pr.Identifier.Source, "/")
   198  
   199  	// We need to verify that the plugin source is in the expected format
   200  	return strings.Join([]string{fmt.Sprintf("packer-plugin-%s", pluginSourceParts[2]),
   201  		"v" + version,
   202  		"x" + g.APIMajor + "." + g.APIMinor,
   203  		entry.Os,
   204  		entry.Arch + ".zip",
   205  	}, "_")
   206  }