Files
hashicorp-copywrite[bot] 19055df3ec [COMPLIANCE] License changes (#12568)
* Updating the license from MPL to Business Source License

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at https://hashi.co/license-faq, and details of the license at www.hashicorp.com/bsl.

* Update copyright file headers to BUSL-1.1

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2023-08-10 15:53:29 -07:00

250 lines
6.4 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package github
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/google/go-github/v33/github"
plugingetter "github.com/hashicorp/packer/packer/plugin-getter"
"golang.org/x/oauth2"
)
const (
ghTokenAccessor = "PACKER_GITHUB_API_TOKEN"
defaultUserAgent = "packer-github-plugin-getter"
defaultHostname = "github.com"
)
type Getter struct {
Client *github.Client
UserAgent string
}
var _ plugingetter.Getter = &Getter{}
func transformChecksumStream() func(in io.ReadCloser) (io.ReadCloser, error) {
return func(in io.ReadCloser) (io.ReadCloser, error) {
defer in.Close()
rd := bufio.NewReader(in)
buffer := bytes.NewBufferString("[")
json := json.NewEncoder(buffer)
for i := 0; ; i++ {
line, err := rd.ReadString('\n')
if err != nil {
if err != io.EOF {
return nil, fmt.Errorf(
"Error reading checksum file: %s", err)
}
break
}
parts := strings.Fields(line)
switch len(parts) {
case 2: // nominal case
checksumString, checksumFilename := parts[0], parts[1]
if i > 0 {
_, _ = buffer.WriteString(",")
}
if err := json.Encode(struct {
Checksum string `json:"checksum"`
Filename string `json:"filename"`
}{
Checksum: checksumString,
Filename: checksumFilename,
}); err != nil {
return nil, err
}
}
}
_, _ = buffer.WriteString("]")
return ioutil.NopCloser(buffer), nil
}
}
// transformVersionStream get a stream from github tags and transforms it into
// something Packer wants, namely a json list of Release.
func transformVersionStream(in io.ReadCloser) (io.ReadCloser, error) {
if in == nil {
return nil, fmt.Errorf("transformVersionStream got nil body")
}
defer in.Close()
dec := json.NewDecoder(in)
m := []struct {
Ref string `json:"ref"`
}{}
if err := dec.Decode(&m); err != nil {
return nil, err
}
out := []plugingetter.Release{}
for _, m := range m {
out = append(out, plugingetter.Release{
Version: strings.TrimPrefix(m.Ref, "refs/tags/"),
})
}
buf := &bytes.Buffer{}
if err := json.NewEncoder(buf).Encode(out); err != nil {
return nil, err
}
return ioutil.NopCloser(buf), nil
}
// HostSpecificTokenAuthTransport makes sure the http roundtripper only sets an
// auth token for requests aimed at a specific host.
//
// This helps for example to get release files from Github as Github will
// redirect to s3 which will error if we give it a Github auth token.
type HostSpecificTokenAuthTransport struct {
// Host to TokenSource map
TokenSources map[string]oauth2.TokenSource
// actual RoundTripper, nil means we use the default one from http.
Base http.RoundTripper
}
// RoundTrip authorizes and authenticates the request with an
// access token from Transport's Source.
func (t *HostSpecificTokenAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
source, found := t.TokenSources[req.Host]
if found {
reqBodyClosed := false
if req.Body != nil {
defer func() {
if !reqBodyClosed {
req.Body.Close()
}
}()
}
if source == nil {
return nil, errors.New("transport's Source is nil")
}
token, err := source.Token()
if err != nil {
return nil, err
}
token.SetAuthHeader(req)
// req.Body is assumed to be closed by the base RoundTripper.
reqBodyClosed = true
}
return t.base().RoundTrip(req)
}
func (t *HostSpecificTokenAuthTransport) base() http.RoundTripper {
if t.Base != nil {
return t.Base
}
return http.DefaultTransport
}
func (g *Getter) Get(what string, opts plugingetter.GetOptions) (io.ReadCloser, error) {
if opts.PluginRequirement.Identifier.Hostname != defaultHostname {
s := opts.PluginRequirement.Identifier.String() + " doesn't appear to be a valid " + defaultHostname + " source address; check source and try again."
return nil, errors.New(s)
}
ctx := context.TODO()
if g.Client == nil {
var tc *http.Client
if tk := os.Getenv(ghTokenAccessor); tk != "" {
log.Printf("[DEBUG] github-getter: using %s", ghTokenAccessor)
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: tk},
)
tc = &http.Client{
Transport: &HostSpecificTokenAuthTransport{
TokenSources: map[string]oauth2.TokenSource{
"api.github.com": ts,
},
},
}
} else {
log.Printf("[WARNING] github-getter: no GitHub token set, if you intend to install plugins often, please set the %s env var", ghTokenAccessor)
}
g.Client = github.NewClient(tc)
g.Client.UserAgent = defaultUserAgent
if g.UserAgent != "" {
g.Client.UserAgent = g.UserAgent
}
}
var req *http.Request
var err error
transform := func(in io.ReadCloser) (io.ReadCloser, error) {
return in, nil
}
switch what {
case "releases":
u := filepath.ToSlash("/repos/" + opts.PluginRequirement.Identifier.RealRelativePath() + "/git/matching-refs/tags")
req, err = g.Client.NewRequest("GET", u, nil)
transform = transformVersionStream
case "sha256":
// something like https://github.com/sylviamoss/packer-plugin-comment/releases/download/v0.2.11/packer-plugin-comment_v0.2.11_x5_SHA256SUMS
u := filepath.ToSlash("https://github.com/" + opts.PluginRequirement.Identifier.RealRelativePath() + "/releases/download/" + opts.Version() + "/" + opts.PluginRequirement.FilenamePrefix() + opts.Version() + "_SHA256SUMS")
req, err = g.Client.NewRequest(
"GET",
u,
nil,
)
transform = transformChecksumStream()
case "zip":
u := filepath.ToSlash("https://github.com/" + opts.PluginRequirement.Identifier.RealRelativePath() + "/releases/download/" + opts.Version() + "/" + opts.ExpectedZipFilename())
req, err = g.Client.NewRequest(
"GET",
u,
nil,
)
default:
return nil, fmt.Errorf("%q not implemented", what)
}
if err != nil {
return nil, err
}
log.Printf("[DEBUG] github-getter: getting %q", req.URL)
resp, err := g.Client.BareDo(ctx, req)
if err != nil {
// here BareDo will return an err if the request failed or if the status
// is not considered a valid http status. So we have to close the body
// if it's not nil.
if resp != nil {
resp.Body.Close()
}
switch err := err.(type) {
case *github.RateLimitError:
return nil, &plugingetter.RateLimitError{
SetableEnvVar: ghTokenAccessor,
Err: err,
ResetTime: err.Rate.Reset.Time,
}
default:
log.Printf("[TRACE] failed requesting: %T. %v", err, err)
return nil, err
}
}
return transform(resp.Body)
}