mirror of
https://github.com/strongdm/comply
synced 2024-08-30 10:53:34 +00:00
449 lines
11 KiB
Go
449 lines
11 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"os"
|
||
|
"strconv"
|
||
|
|
||
|
"github.com/aktau/github-release/github"
|
||
|
)
|
||
|
|
||
|
func infocmd(opt Options) error {
|
||
|
user := nvls(opt.Info.User, EnvUser)
|
||
|
repo := nvls(opt.Info.Repo, EnvRepo)
|
||
|
token := nvls(opt.Info.Token, EnvToken)
|
||
|
tag := opt.Info.Tag
|
||
|
|
||
|
if user == "" || repo == "" {
|
||
|
return fmt.Errorf("user and repo need to be passed as arguments")
|
||
|
}
|
||
|
|
||
|
// Find regular git tags.
|
||
|
foundTags, err := Tags(user, repo, token)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("could not fetch tags, %v", err)
|
||
|
}
|
||
|
if len(foundTags) == 0 {
|
||
|
return fmt.Errorf("no tags available for %v/%v", user, repo)
|
||
|
}
|
||
|
|
||
|
tags := foundTags[:0]
|
||
|
for _, t := range foundTags {
|
||
|
// If the user only requested one tag, filter out the rest.
|
||
|
if tag == "" || t.Name == tag {
|
||
|
tags = append(tags, t)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
renderer := renderInfoText
|
||
|
|
||
|
if opt.Info.JSON {
|
||
|
renderer = renderInfoJSON
|
||
|
}
|
||
|
|
||
|
// List releases + assets.
|
||
|
var releases []Release
|
||
|
if tag == "" {
|
||
|
// Get all releases.
|
||
|
vprintf("%v/%v: getting information for all releases\n", user, repo)
|
||
|
releases, err = Releases(user, repo, token)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
} else {
|
||
|
// Get only one release.
|
||
|
vprintf("%v/%v/%v: getting information for the release\n", user, repo, tag)
|
||
|
release, err := ReleaseOfTag(user, repo, tag, token)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
releases = []Release{*release}
|
||
|
}
|
||
|
|
||
|
return renderer(tags, releases)
|
||
|
}
|
||
|
|
||
|
func renderInfoText(tags []Tag, releases []Release) error {
|
||
|
fmt.Println("tags:")
|
||
|
for _, tag := range tags {
|
||
|
fmt.Println("-", &tag)
|
||
|
}
|
||
|
|
||
|
fmt.Println("releases:")
|
||
|
for _, release := range releases {
|
||
|
fmt.Println("-", &release)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func renderInfoJSON(tags []Tag, releases []Release) error {
|
||
|
out := struct {
|
||
|
Tags []Tag
|
||
|
Releases []Release
|
||
|
}{
|
||
|
Tags: tags,
|
||
|
Releases: releases,
|
||
|
}
|
||
|
|
||
|
enc := json.NewEncoder(os.Stdout)
|
||
|
enc.SetIndent("", " ")
|
||
|
return enc.Encode(&out)
|
||
|
}
|
||
|
|
||
|
func uploadcmd(opt Options) error {
|
||
|
user := nvls(opt.Upload.User, EnvUser)
|
||
|
repo := nvls(opt.Upload.Repo, EnvRepo)
|
||
|
token := nvls(opt.Upload.Token, EnvToken)
|
||
|
tag := opt.Upload.Tag
|
||
|
name := opt.Upload.Name
|
||
|
label := opt.Upload.Label
|
||
|
file := opt.Upload.File
|
||
|
|
||
|
vprintln("uploading...")
|
||
|
|
||
|
if file == nil {
|
||
|
return fmt.Errorf("provided file was not valid")
|
||
|
}
|
||
|
defer file.Close()
|
||
|
|
||
|
if err := ValidateCredentials(user, repo, token, tag); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Find the release corresponding to the entered tag, if any.
|
||
|
rel, err := ReleaseOfTag(user, repo, tag, token)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// If asked to replace, first delete the existing asset, if any.
|
||
|
if assetID := findAssetID(rel.Assets, name); opt.Upload.Replace && assetID != -1 {
|
||
|
URL := nvls(EnvApiEndpoint, github.DefaultBaseURL) +
|
||
|
fmt.Sprintf(ASSET_DOWNLOAD_URI, user, repo, assetID)
|
||
|
resp, err := github.DoAuthRequest("DELETE", URL, "application/json", token, nil, nil)
|
||
|
if err != nil || resp.StatusCode != http.StatusNoContent {
|
||
|
return fmt.Errorf("could not replace asset %s (ID: %d), deletion failed (error: %v, status: %s)",
|
||
|
name, assetID, err, resp.Status)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
v := url.Values{}
|
||
|
v.Set("name", name)
|
||
|
if label != "" {
|
||
|
v.Set("label", label)
|
||
|
}
|
||
|
|
||
|
url := rel.CleanUploadUrl() + "?" + v.Encode()
|
||
|
|
||
|
resp, err := github.DoAuthRequest("POST", url, "application/octet-stream",
|
||
|
token, nil, file)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("can't create upload request to %v, %v", url, err)
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
vprintln("RESPONSE:", resp)
|
||
|
if resp.StatusCode != http.StatusCreated {
|
||
|
if msg, err := ToMessage(resp.Body); err == nil {
|
||
|
return fmt.Errorf("could not upload, status code (%v), %v",
|
||
|
resp.Status, msg)
|
||
|
}
|
||
|
return fmt.Errorf("could not upload, status code (%v)", resp.Status)
|
||
|
}
|
||
|
|
||
|
if VERBOSITY != 0 {
|
||
|
vprintf("BODY: ")
|
||
|
if _, err := io.Copy(os.Stderr, resp.Body); err != nil {
|
||
|
return fmt.Errorf("while reading response, %v", err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func downloadcmd(opt Options) error {
|
||
|
user := nvls(opt.Download.User, EnvUser)
|
||
|
repo := nvls(opt.Download.Repo, EnvRepo)
|
||
|
token := nvls(opt.Download.Token, EnvToken)
|
||
|
tag := opt.Download.Tag
|
||
|
name := opt.Download.Name
|
||
|
latest := opt.Download.Latest
|
||
|
|
||
|
vprintln("downloading...")
|
||
|
|
||
|
if err := ValidateTarget(user, repo, tag, latest); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Find the release corresponding to the entered tag, if any.
|
||
|
var rel *Release
|
||
|
var err error
|
||
|
if latest {
|
||
|
rel, err = LatestRelease(user, repo, token)
|
||
|
} else {
|
||
|
rel, err = ReleaseOfTag(user, repo, tag, token)
|
||
|
}
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
assetID := findAssetID(rel.Assets, name)
|
||
|
if assetID == -1 {
|
||
|
return fmt.Errorf("coud not find asset named %s", name)
|
||
|
}
|
||
|
|
||
|
var resp *http.Response
|
||
|
if token == "" {
|
||
|
// Use the regular github.com site it we don't have a token.
|
||
|
resp, err = http.Get("https://github.com" + fmt.Sprintf("/%s/%s/releases/download/%s/%s", user, repo, tag, name))
|
||
|
} else {
|
||
|
url := nvls(EnvApiEndpoint, github.DefaultBaseURL) + fmt.Sprintf(ASSET_DOWNLOAD_URI, user, repo, assetID)
|
||
|
resp, err = github.DoAuthRequest("GET", url, "", token, map[string]string{
|
||
|
"Accept": "application/octet-stream",
|
||
|
}, nil)
|
||
|
}
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("could not fetch releases, %v", err)
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
vprintln("GET", resp.Request.URL, "->", resp)
|
||
|
|
||
|
contentLength, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if resp.StatusCode != http.StatusOK {
|
||
|
return fmt.Errorf("github did not respond with 200 OK but with %v", resp.Status)
|
||
|
}
|
||
|
|
||
|
out := os.Stdout // Pipe the asset to stdout by default.
|
||
|
if isCharDevice(out) {
|
||
|
// If stdout is a char device, assume it's a TTY (terminal). In this
|
||
|
// case, don't pipe th easset to stdout, but create it as a file in
|
||
|
// the current working folder.
|
||
|
if out, err = os.Create(name); err != nil {
|
||
|
return fmt.Errorf("could not create file %s", name)
|
||
|
}
|
||
|
defer out.Close()
|
||
|
}
|
||
|
|
||
|
return mustCopyN(out, resp.Body, contentLength)
|
||
|
}
|
||
|
|
||
|
// mustCopyN attempts to copy exactly N bytes, if this fails, an error is
|
||
|
// returned.
|
||
|
func mustCopyN(w io.Writer, r io.Reader, n int64) error {
|
||
|
an, err := io.Copy(w, r)
|
||
|
if an != n {
|
||
|
return fmt.Errorf("data did not match content length %d != %d", an, n)
|
||
|
}
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
func ValidateTarget(user, repo, tag string, latest bool) error {
|
||
|
if user == "" {
|
||
|
return fmt.Errorf("empty user")
|
||
|
}
|
||
|
if repo == "" {
|
||
|
return fmt.Errorf("empty repo")
|
||
|
}
|
||
|
if tag == "" && !latest {
|
||
|
return fmt.Errorf("empty tag")
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func ValidateCredentials(user, repo, token, tag string) error {
|
||
|
if err := ValidateTarget(user, repo, tag, false); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if token == "" {
|
||
|
return fmt.Errorf("empty token")
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func releasecmd(opt Options) error {
|
||
|
cmdopt := opt.Release
|
||
|
user := nvls(cmdopt.User, EnvUser)
|
||
|
repo := nvls(cmdopt.Repo, EnvRepo)
|
||
|
token := nvls(cmdopt.Token, EnvToken)
|
||
|
tag := cmdopt.Tag
|
||
|
name := nvls(cmdopt.Name, tag)
|
||
|
desc := nvls(cmdopt.Desc, tag)
|
||
|
target := nvls(cmdopt.Target)
|
||
|
draft := cmdopt.Draft
|
||
|
prerelease := cmdopt.Prerelease
|
||
|
|
||
|
vprintln("releasing...")
|
||
|
|
||
|
if err := ValidateCredentials(user, repo, token, tag); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Check if we need to read the description from stdin.
|
||
|
if desc == "-" {
|
||
|
b, err := ioutil.ReadAll(os.Stdin)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("could not read description from stdin: %v", err)
|
||
|
}
|
||
|
desc = string(b)
|
||
|
}
|
||
|
|
||
|
params := ReleaseCreate{
|
||
|
TagName: tag,
|
||
|
TargetCommitish: target,
|
||
|
Name: name,
|
||
|
Body: desc,
|
||
|
Draft: draft,
|
||
|
Prerelease: prerelease,
|
||
|
}
|
||
|
|
||
|
/* encode params as json */
|
||
|
payload, err := json.Marshal(params)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("can't encode release creation params, %v", err)
|
||
|
}
|
||
|
reader := bytes.NewReader(payload)
|
||
|
|
||
|
URL := nvls(EnvApiEndpoint, github.DefaultBaseURL) + fmt.Sprintf("/repos/%s/%s/releases", user, repo)
|
||
|
resp, err := github.DoAuthRequest("POST", URL, "application/json", token, nil, reader)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("while submitting %v, %v", string(payload), err)
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
vprintln("RESPONSE:", resp)
|
||
|
if resp.StatusCode != http.StatusCreated {
|
||
|
if resp.StatusCode == 422 {
|
||
|
return fmt.Errorf("github returned %v (this is probably because the release already exists)",
|
||
|
resp.Status)
|
||
|
}
|
||
|
return fmt.Errorf("github returned %v", resp.Status)
|
||
|
}
|
||
|
|
||
|
if VERBOSITY != 0 {
|
||
|
body, err := ioutil.ReadAll(resp.Body)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("error while reading response, %v", err)
|
||
|
}
|
||
|
vprintln("BODY:", string(body))
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func editcmd(opt Options) error {
|
||
|
cmdopt := opt.Edit
|
||
|
user := nvls(cmdopt.User, EnvUser)
|
||
|
repo := nvls(cmdopt.Repo, EnvRepo)
|
||
|
token := nvls(cmdopt.Token, EnvToken)
|
||
|
tag := cmdopt.Tag
|
||
|
name := nvls(cmdopt.Name, tag)
|
||
|
desc := nvls(cmdopt.Desc, tag)
|
||
|
draft := cmdopt.Draft
|
||
|
prerelease := cmdopt.Prerelease
|
||
|
|
||
|
vprintln("editing...")
|
||
|
|
||
|
if err := ValidateCredentials(user, repo, token, tag); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
id, err := IdOfTag(user, repo, tag, token)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
vprintf("release %v has id %v\n", tag, id)
|
||
|
|
||
|
// Check if we need to read the description from stdin.
|
||
|
if desc == "-" {
|
||
|
b, err := ioutil.ReadAll(os.Stdin)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("could not read description from stdin: %v", err)
|
||
|
}
|
||
|
desc = string(b)
|
||
|
}
|
||
|
|
||
|
/* the release create struct works for editing releases as well */
|
||
|
params := ReleaseCreate{
|
||
|
TagName: tag,
|
||
|
Name: name,
|
||
|
Body: desc,
|
||
|
Draft: draft,
|
||
|
Prerelease: prerelease,
|
||
|
}
|
||
|
|
||
|
/* encode the parameters as JSON, as required by the github API */
|
||
|
payload, err := json.Marshal(params)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("can't encode release creation params, %v", err)
|
||
|
}
|
||
|
|
||
|
URL := nvls(EnvApiEndpoint, github.DefaultBaseURL) + fmt.Sprintf("/repos/%s/%s/releases/%d", user, repo, id)
|
||
|
resp, err := github.DoAuthRequest("PATCH", URL, "application/json", token, nil, bytes.NewReader(payload))
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("while submitting %v, %v", string(payload), err)
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
vprintln("RESPONSE:", resp)
|
||
|
if resp.StatusCode != http.StatusOK {
|
||
|
if resp.StatusCode == 422 {
|
||
|
return fmt.Errorf("github returned %v (this is probably because the release already exists)",
|
||
|
resp.Status)
|
||
|
}
|
||
|
return fmt.Errorf("github returned unexpected status code %v", resp.Status)
|
||
|
}
|
||
|
|
||
|
if VERBOSITY != 0 {
|
||
|
body, err := ioutil.ReadAll(resp.Body)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("error while reading response, %v", err)
|
||
|
}
|
||
|
vprintln("BODY:", string(body))
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func deletecmd(opt Options) error {
|
||
|
user, repo, token, tag := nvls(opt.Delete.User, EnvUser),
|
||
|
nvls(opt.Delete.Repo, EnvRepo),
|
||
|
nvls(opt.Delete.Token, EnvToken),
|
||
|
opt.Delete.Tag
|
||
|
vprintln("deleting...")
|
||
|
|
||
|
id, err := IdOfTag(user, repo, tag, token)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
vprintf("release %v has id %v\n", tag, id)
|
||
|
|
||
|
baseURL := nvls(EnvApiEndpoint, github.DefaultBaseURL)
|
||
|
resp, err := github.DoAuthRequest("DELETE", baseURL+fmt.Sprintf("/repos/%s/%s/releases/%d",
|
||
|
user, repo, id), "application/json", token, nil, nil)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("release deletion failed: %v", err)
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
if resp.StatusCode != http.StatusNoContent {
|
||
|
return fmt.Errorf("could not delete the release corresponding to tag %s on repo %s/%s",
|
||
|
tag, user, repo)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|