Snap for 12740440 from 60f7c64b7a22a30065e034ac1ed9e5bdc0d47c68 to androidx-activity-release
Change-Id: Ibbde0a2a4132ac56a342560c0e90080712be2071
diff --git a/README.md b/README.md
index e23d01b..f9939fd 100644
--- a/README.md
+++ b/README.md
@@ -6,13 +6,18 @@
## Options
* `target`: **Required** - The target you would like to download the artifact from.
-* `build_id`: **Required** - The build_id of the target to download the artifact from.
* `artifact`: **Required** - The artifact to download.
-* `output`: *Optional* - If you would like the contents of the file to be written to a specific file
+* **Required**: either `build_id` or `branch`, but not both
+ * When only `build_id` is provided, the script would download the artifact from that `build_id`.
+ * When only `branch` is provided, the script would download the artifact from the last known good build of that `branch`.
+* `output`: *Optional* - If you would like the contents of the file to be written to a specific file.
+* `client_id`: *Optional* - If authorization is required to download the artifact, please set this parameter as your OAuth2.0 Client ID.
+* `secret`: *Optional* - If authorization is required to download the artifact, please set this parameter as your OAuth2.0 Client secret.
+* `port`: *Optional* - If you would like to specify the OAuth callback port to listen on. Default: 10502
+* `project_id`: *Optional* - The project id being used to access the fetch APIs.
* `-`: *Optional* - If you would like the contents of the file to be written to stdout (must be the last arg)
-
-## Example useage
+## Example usage
```
fetch_artifact -target=aosp_arm64-userdebug -build_id=7000390 -artifact=COPIED
@@ -24,6 +29,24 @@
fetch_artifact -target=aosp_arm64-userdebug -build_id=7000390 -artifact=COPIED -
```
+### Get the latest successful build's artifact without specifying a build_id
+```
+fetch_artifact -target=aosp_arm64-trunk_staging-userdebug -branch=aosp-main -artifact=COPIED
+```
+
+### Using OAuth to fetch restricted artifacts
+In this case, you might need to create an OAuth 2.0 Client ID for a web application and set the redirect URI to `http://localhost:<port>`(default port: 10502).
+
+```
+fetch_artifact -target=<restricted_target> -build_id=<id> -artifact=COPIED -client_id=<OAuth_client_id> -secret=<OAuth_client_secret>
+```
+
+If you are accessing the fetch APIs from a different project than your OAuth client, you will need to specify the `-project_id` flag:
+
+```
+fetch_artifact -target=<restricted_target> -build_id=<id> -artifact=COPIED -client_id=<OAuth_client_id> -secret=<OAuth_client_secret> -project_id=<project_id>
+```
+
## Development
### Building
diff --git a/fetch_artifact.go b/fetch_artifact.go
index 843864c..9370698 100644
--- a/fetch_artifact.go
+++ b/fetch_artifact.go
@@ -15,21 +15,140 @@
package main
import (
+ "context"
+ "encoding/json"
+ "errors"
"flag"
"fmt"
"io"
"io/ioutil"
+ "log"
"net/http"
+ "net/url"
"os"
"path"
+ "time"
+
+ "golang.org/x/oauth2"
+ "golang.org/x/oauth2/google"
)
-var target = flag.String("target", "", "the target to fetch from")
-var buildID = flag.String("build_id", "", "the build id to fetch from")
-var artifact = flag.String("artifact", "", "the artifact to download")
-var output = flag.String("output", "", "the file name to save as")
+var (
+ target = flag.String("target", "", "the target to fetch from")
+ buildID = flag.String("build_id", "", "the build id to fetch from, can use '-branch' to get the latest passed build ID")
+ branch = flag.String("branch", "", "the branch to fetch from, used when '-build_id' is not provided,\nit would fetch the latest successful build")
+ artifact = flag.String("artifact", "", "the artifact to download")
+ output = flag.String("output", "", "the file name to save as")
+ clientID = flag.String("client_id", "", "[Optional] OAuth 2.0 Client ID. Must be used with '-secret'")
+ secret = flag.String("secret", "", "[Optional] OAuth 2.0 Client Secret. Must be used with '-client_id'")
+ port = flag.Int("port", 10502, "[Optional] the port number where the oauth callback server would listen on")
+ projectID = flag.String("project_id", "", "[Optional] the project id being used to access the fetch APIs.")
+)
+
var writeToStdout = false
+type BuildResponse struct {
+ Builds []Build `json:"builds"`
+}
+
+type Build struct {
+ BuildId string `json:"buildId"`
+}
+
+type Auth struct {
+ clientID string
+ secret string
+}
+
+func newAuth(clientID, secret string) *Auth {
+ return &Auth{clientID: clientID, secret: secret}
+}
+
+func newClient(ctx context.Context, auth *Auth) *http.Client {
+ if auth == nil {
+ return &http.Client{}
+ }
+
+ config := &oauth2.Config{
+ ClientID: auth.clientID,
+ ClientSecret: auth.secret,
+ Endpoint: google.Endpoint,
+ Scopes: []string{"https://www.googleapis.com/auth/androidbuild.internal"},
+ }
+ return newOAuthClient(ctx, config)
+}
+
+type FetchConfig struct {
+ client *http.Client
+ target string
+ buildID string
+ branch string
+ output string
+ artifact string
+ projectID string
+ args []string
+}
+
+func newFetchConfig(client *http.Client, target string, buildID string, branch string, output string, artifact string, projectID string, args []string) (*FetchConfig, error) {
+ config := &FetchConfig{
+ client: client,
+ target: target,
+ buildID: buildID,
+ branch: branch,
+ output: output,
+ artifact: artifact,
+ projectID: projectID,
+ args: args,
+ }
+ err := config.validate()
+ if err != nil {
+ return nil, err
+ }
+ return config, nil
+}
+
+func (c *FetchConfig) validate() error {
+ // We only support passing 1 argument `-` so if we have more than
+ // one argument this is an error state,
+ if len(c.args) > 1 {
+ return errors.New("too many arguments passed to fetch_artifact")
+ }
+
+ if len(c.args) > 0 {
+ writeToStdout = c.args[len(c.args)-1] == "-"
+ if !writeToStdout {
+ return fmt.Errorf(
+ "only supported final argument to fetch_artifact is `-` but got `%s`", c.args[len(c.args)-1])
+ }
+
+ if len(c.output) > 0 && writeToStdout {
+ return errors.New("both '-output' and '-' flags can not be used together")
+ }
+ }
+
+ // check user provided flags
+ if len(c.target) == 0 {
+ return errors.New("missing target")
+ }
+ if len(c.artifact) == 0 {
+ return errors.New("missing artifact")
+ }
+ if len(c.buildID) == 0 && len(c.branch) == 0 {
+ return errors.New("missing build_id or branch")
+ }
+ if len(c.buildID) != 0 && len(c.branch) != 0 {
+ return errors.New("too many arguments, you should only give build_id or branch")
+ }
+ if len(c.buildID) == 0 {
+ bid, err := getLatestGoodBuild(c)
+ if err != nil {
+ return err
+ }
+ c.buildID = bid
+ }
+ return nil
+}
+
func errPrint(msg string) {
fmt.Fprintln(os.Stderr, msg)
os.Exit(1)
@@ -38,36 +157,106 @@
func main() {
flag.Parse()
args := flag.Args()
- // We only support passing 1 argument `-` so if we have more than
- // one argument this is an error state,
- if len(args) > 1 {
- errPrint("Error: Too many arguments passed to fetch_artifact.")
+
+ var auth *Auth
+ if len(*clientID) != 0 && len(*secret) != 0 {
+ auth = newAuth(*clientID, *secret)
+ } else if len(*clientID) != 0 || len(*secret) != 0 {
+ errPrint(fmt.Sprintf("missing client_id or client_secret."))
}
- if len(args) > 0 {
- writeToStdout = args[len(args)-1] == "-"
- if !writeToStdout {
- errPrint(fmt.Sprintf(
- "Error: Only supported final argument to fetch_artifact is `-` but got `%s`.", args[len(args)-1]))
- }
+ ctx := context.Background()
+ client := newClient(ctx, auth)
- if len(*output) > 0 && writeToStdout {
- errPrint("Error: Both '-output' and '-' flags can not be used together.")
- }
+ config, err := newFetchConfig(client, *target, *buildID, *branch, *output, *artifact, *projectID, args)
+ if err != nil {
+ errPrint(fmt.Sprintf("Config validation error: %s", err))
}
- url := fmt.Sprintf("https://androidbuildinternal.googleapis.com/android/internal/build/v3/builds/%s/%s/attempts/latest/artifacts/%s/url", *buildID, *target, *artifact)
+ fetchArtifact(config)
+}
+func newOAuthClient(ctx context.Context, config *oauth2.Config) *http.Client {
+ token := tokenFromWeb(ctx, config)
+ return config.Client(ctx, token)
+}
+
+func tokenFromWeb(ctx context.Context, config *oauth2.Config) *oauth2.Token {
+ ch := make(chan string)
+ randState := fmt.Sprintf("st%d", time.Now().UnixNano())
+ ts := createServer(ch, randState)
+ go func() {
+ err := ts.ListenAndServe()
+ if err != http.ErrServerClosed {
+ errPrint(fmt.Sprintf("Listen and serve error: %v", err))
+ }
+ }()
+
+ defer ts.Close()
+ config.RedirectURL = "http://localhost" + ts.Addr
+ authURL := config.AuthCodeURL(randState)
+ log.Printf("Authorize this app at: %s", authURL)
+ code := <-ch
+
+ token, err := config.Exchange(ctx, code)
+ if err != nil {
+ errPrint(fmt.Sprintf("Token exchange error: %v", err))
+
+ }
+ return token
+}
+
+func createServer(ch chan string, state string) *http.Server {
+ return &http.Server{
+ Addr: fmt.Sprintf(":%d", *port),
+ Handler: http.HandlerFunc(handler(ch, state)),
+ }
+}
+
+func handler(ch chan string, randState string) func(http.ResponseWriter, *http.Request) {
+ return func(rw http.ResponseWriter, req *http.Request) {
+ if req.URL.Path == "/favicon.ico" {
+ http.Error(rw, "error: visiting /favicon.ico", 404)
+ return
+ }
+ if req.FormValue("state") != randState {
+ log.Printf("state: %s doesn't match. (expected: %s)", req.FormValue("state"), randState)
+ http.Error(rw, "invalid state", 500)
+ return
+ }
+ if code := req.FormValue("code"); code != "" {
+ fmt.Fprintf(rw, "<h1>Success</h1>Authorized.")
+ rw.(http.Flusher).Flush()
+ ch <- code
+ return
+ }
+ ch <- ""
+ http.Error(rw, "invalid code", 500)
+ }
+}
+
+func sendRequest(client *http.Client, url string) (*http.Response, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
- errPrint(fmt.Sprintf("unable to build request %v", err))
+ return nil, err
}
req.Header.Set("Accept", "application/json")
- client := http.Client{}
res, err := client.Do(req)
if err != nil {
- errPrint(fmt.Sprintf("Unable to make request %s.", err))
+ return nil, err
+ }
+ return res, nil
+}
+
+func fetchArtifact(c *FetchConfig) error {
+ apiURL := fmt.Sprintf("https://androidbuildinternal.googleapis.com/android/internal/build/v3/builds/%s/%s/attempts/latest/artifacts/%s/url", url.QueryEscape(c.buildID), url.QueryEscape(c.target), url.QueryEscape(c.artifact))
+ if len(c.projectID) != 0 {
+ apiURL += fmt.Sprintf("?$userProject=%s", url.QueryEscape(c.projectID))
+ }
+ res, err := sendRequest(c.client, apiURL)
+ if err != nil {
+ return fmt.Errorf("error fetching artifact %w", err)
}
defer res.Body.Close()
@@ -78,19 +267,50 @@
if writeToStdout {
io.Copy(os.Stdout, res.Body)
- return
+ return nil
}
- fileName := *artifact
- if len(*output) > 0 {
- fileName = *output
+ fileName := c.artifact
+ if len(c.output) > 0 {
+ fileName = c.output
}
f, err := os.Create(path.Base(fileName))
if err != nil {
- errPrint(fmt.Sprintf("Unable to create file %s.", err))
+ return fmt.Errorf("unable to create file %w", err)
}
defer f.Close()
io.Copy(f, res.Body)
fmt.Printf("File %s created.\n", f.Name())
+ return nil
+}
+
+func getLatestGoodBuild(c *FetchConfig) (string, error) {
+ apiURL := fmt.Sprintf("https://androidbuildinternal.googleapis.com/android/internal/build/v3/builds?branches=%s&buildAttemptStatus=complete&buildType=submitted&maxResults=1&successful=true&target=%s", url.QueryEscape(c.branch), url.QueryEscape(c.target))
+ if len(c.projectID) != 0 {
+ apiURL += fmt.Sprintf("&$userProject=%s", url.QueryEscape(c.projectID))
+ }
+ res, err := sendRequest(c.client, apiURL)
+ if err != nil {
+ return "", fmt.Errorf("send request error: %w", err)
+ }
+ defer res.Body.Close()
+
+ if res.Status != "200 OK" {
+ body, _ := ioutil.ReadAll(res.Body)
+ return "", fmt.Errorf("unable to get Build ID: %s\n %s", res.Status, body)
+ }
+
+ body, _ := io.ReadAll(res.Body)
+ var buildData BuildResponse
+ err = json.Unmarshal(body, &buildData)
+
+ if err != nil {
+ return "", fmt.Errorf("error parsing JSON: %w", err)
+ }
+ if len(buildData.Builds) == 0 {
+ return "", errors.New("error no build ID is found")
+ }
+
+ return buildData.Builds[0].BuildId, nil
}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..df28410
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,7 @@
+module fetch_artifact
+
+go 1.23
+
+require golang.org/x/oauth2 v0.23.0
+
+require cloud.google.com/go/compute/metadata v0.3.0 // indirect
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..9666e99
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,6 @@
+cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
+cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
+golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=