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 }