Builder manager bugfixes (#367)

This contains a few different builder manager fixes:

* Update the zip file structure to avoid an extra subdirectory
* Annotate packages with what functions are using them
* Only trigger builds when there is no deployment archive
* Python environment loadpath bugfixes
* Other bugfixes
This commit is contained in:
Ta-Ching Chen
2017-11-06 23:01:46 -08:00
committed by Soam Vasani
parent 380b7d1bcd
commit a63b8b372f
13 changed files with 150 additions and 81 deletions
+10
View File
@@ -269,22 +269,32 @@ func (envw *environmentWatcher) createBuilder(env *crd.Environment) (*builderInf
if err != nil { if err != nil {
return nil, err return nil, err
} }
// there should be only one service in svcList
if len(svcList) == 0 { if len(svcList) == 0 {
svc, err = envw.createBuilderService(env) svc, err = envw.createBuilderService(env)
if err != nil { if err != nil {
return nil, fmt.Errorf("Error creating builder service: %v", err) return nil, fmt.Errorf("Error creating builder service: %v", err)
} }
} else if len(svcList) == 1 {
svc = &svcList[0]
} else {
return nil, fmt.Errorf("Found more than one builder service for environment %v", env.Metadata.Name)
} }
deployList, err := envw.getBuilderDeploymentList(sel) deployList, err := envw.getBuilderDeploymentList(sel)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// there should be only one deploy in deployList
if len(deployList) == 0 { if len(deployList) == 0 {
deploy, err = envw.createBuilderDeployment(env) deploy, err = envw.createBuilderDeployment(env)
if err != nil { if err != nil {
return nil, fmt.Errorf("Error creating builder deployment: %v", err) return nil, fmt.Errorf("Error creating builder deployment: %v", err)
} }
} else if len(deployList) == 1 {
deploy = &deployList[0]
} else {
return nil, fmt.Errorf("Found more than one builder deployment for environment %v", env.Metadata.Name)
} }
return &builderInfo{ return &builderInfo{
-1
View File
@@ -181,7 +181,6 @@ func (api *API) Serve(port int) {
r.HandleFunc("/proxy/{dbType}", api.FunctionLogsApiPost).Methods("POST") r.HandleFunc("/proxy/{dbType}", api.FunctionLogsApiPost).Methods("POST")
r.HandleFunc("/proxy/storage/v1/archive", api.StorageServiceProxy) r.HandleFunc("/proxy/storage/v1/archive", api.StorageServiceProxy)
r.HandleFunc("/proxy/buildermgr/v1/build", api.BuilderManagerBuildProxy) r.HandleFunc("/proxy/buildermgr/v1/build", api.BuilderManagerBuildProxy)
r.HandleFunc("/proxy/buildermgr/v1/builder", api.BuilderManagerEnvBuilderProxy)
r.HandleFunc("/proxy/logs/{function}", api.FunctionPodLogs).Methods("POST") r.HandleFunc("/proxy/logs/{function}", api.FunctionPodLogs).Methods("POST")
r.HandleFunc("/proxy/workflows-apiserver/{path:.*}", api.WorkflowApiserverProxy) r.HandleFunc("/proxy/workflows-apiserver/{path:.*}", api.WorkflowApiserverProxy)
+2 -14
View File
@@ -26,7 +26,7 @@ import (
func (api *API) BuilderManagerBuildProxy(w http.ResponseWriter, r *http.Request) { func (api *API) BuilderManagerBuildProxy(w http.ResponseWriter, r *http.Request) {
u := api.builderManagerUrl + "/v1/build" u := api.builderManagerUrl + "/v1/build"
proxy, err := api._getBuilderManagerProxy(u) proxy, err := api.getBuilderManagerProxy(u)
if err != nil { if err != nil {
msg := fmt.Sprintf("Failed to establish proxy server: %v", err) msg := fmt.Sprintf("Failed to establish proxy server: %v", err)
log.Println(msg) log.Println(msg)
@@ -36,19 +36,7 @@ func (api *API) BuilderManagerBuildProxy(w http.ResponseWriter, r *http.Request)
proxy.ServeHTTP(w, r) proxy.ServeHTTP(w, r)
} }
func (api *API) BuilderManagerEnvBuilderProxy(w http.ResponseWriter, r *http.Request) { func (api *API) getBuilderManagerProxy(targetUrl string) (*httputil.ReverseProxy, error) {
u := api.builderManagerUrl + "/v1/builder"
proxy, err := api._getBuilderManagerProxy(u)
if err != nil {
msg := fmt.Sprintf("Failed to establish proxy server: %v", err)
log.Println(msg)
http.Error(w, msg, 500)
return
}
proxy.ServeHTTP(w, r)
}
func (api *API) _getBuilderManagerProxy(targetUrl string) (*httputil.ReverseProxy, error) {
svcUrl, err := url.Parse(targetUrl) svcUrl, err := url.Parse(targetUrl)
if err != nil { if err != nil {
return nil, err return nil, err
+3 -6
View File
@@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
//"time" //"time"
@@ -68,13 +67,11 @@ func (c *Client) Upload(fr *fetcher.UploadRequest) (*fetcher.UploadResponse, err
return nil, err return nil, err
} }
log.Printf("Received upload response: %v", string(rBody)) uploadResp := fetcher.UploadResponse{}
err = json.Unmarshal([]byte(rBody), &uploadResp)
uploadReq := fetcher.UploadResponse{}
err = json.Unmarshal([]byte(rBody), &uploadReq)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &uploadReq, nil return &uploadResp, nil
} }
+2 -4
View File
@@ -152,13 +152,11 @@ func (fetcher *Fetcher) FetchHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), 400) http.Error(w, err.Error(), 400)
return return
} }
log.Printf("fetcher received fetch request: %v", req) log.Printf("fetcher received fetch request and started downloading: %v", req)
tmpFile := req.Filename + ".tmp" tmpFile := req.Filename + ".tmp"
tmpPath := filepath.Join(fetcher.sharedVolumePath, tmpFile) tmpPath := filepath.Join(fetcher.sharedVolumePath, tmpFile)
log.Printf("Start downloading...")
if req.FetchType == FETCH_URL { if req.FetchType == FETCH_URL {
// fetch the file and save it to the tmp path // fetch the file and save it to the tmp path
err := downloadUrl(req.Url, tmpPath) err := downloadUrl(req.Url, tmpPath)
@@ -283,7 +281,7 @@ func (fetcher *Fetcher) UploadHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
log.Println("Start uploading...") log.Println("Starting upload...")
ssClient := storageSvcClient.MakeClient(req.StorageSvcUrl) ssClient := storageSvcClient.MakeClient(req.StorageSvcUrl)
fileID, err := ssClient.Upload(dstFilepath, nil) fileID, err := ssClient.Upload(dstFilepath, nil)
+35 -6
View File
@@ -24,12 +24,41 @@ def loadv2():
global userfunc global userfunc
body = request.get_json() body = request.get_json()
filepath = body['filepath'] filepath = body['filepath']
functionName = body['functionName'] handler = body['functionName']
# add filepath into syspath for module import
sys.path.append(filepath) # The value of "functionName" is consist of `<module-name>.<function-name>`.
fn, path, desc = imp.find_module('user', [filepath]) moduleName, funcName = handler.split(".")
mod = imp.load_module('user', fn, path, desc)
userfunc = getattr(mod, functionName) # check whether the destination is a directory or a file
if os.path.isdir(filepath):
# add package directory path into module search path
sys.path.append(filepath)
# find module from package path we append previously.
# Python will try to find module from the same name file under
# the package directory. If search is successful, the return
# value is a 3-element tuple; otherwise, an exception "ImportError"
# is raised.
# Second parameter of find_module enforces python to find same
# name module from the given list of directories to prevent name
# confliction with built-in modules.
f, path, desc = imp.find_module(moduleName, [filepath])
# load module
# Return module object is the load is successful; otherwise,
# an exception is raised.
try:
mod = imp.load_module(moduleName, f, path, desc)
finally:
if f:
f.close()
else:
# load source from destination python file
mod = imp.load_source(moduleName, filepath)
# load user function from module
userfunc = getattr(mod, funcName)
return "" return ""
@app.route('/', methods=['GET', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'DELETE']) @app.route('/', methods=['GET', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'DELETE'])
+1 -2
View File
@@ -55,7 +55,6 @@ func envCreate(c *cli.Context) error {
// Environment API interface version is not specified and // Environment API interface version is not specified and
// builder image is empty, set default interface version // builder image is empty, set default interface version
if envVersion == 0 { if envVersion == 0 {
fmt.Println("Use default environment v1 API interface")
envVersion = 1 envVersion = 1
} }
@@ -132,7 +131,7 @@ func envUpdate(c *cli.Context) error {
} }
if env.Spec.Version == 1 && (len(envBuilderImg) > 0 || len(envBuildCmd) > 0) { if env.Spec.Version == 1 && (len(envBuilderImg) > 0 || len(envBuildCmd) > 0) {
fatal("Environment v1 API interface doesn't supported environment builder.") fatal("Version 1 Environments do not support builders. Must specify --version=2.")
} }
if len(envBuilderImg) > 0 { if len(envBuilderImg) > 0 {
+25 -14
View File
@@ -20,6 +20,7 @@ import (
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@@ -31,6 +32,7 @@ import (
"text/tabwriter" "text/tabwriter"
"time" "time"
"github.com/dchest/uniuri"
"github.com/satori/go.uuid" "github.com/satori/go.uuid"
"github.com/urfave/cli" "github.com/urfave/cli"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -87,36 +89,48 @@ func createArchive(client *client.Client, fileName string) *fission.Archive {
return &archive return &archive
} }
func createPackage(client *client.Client, envName, srcArchiveName, deployArchiveName, buildcmd string) *metav1.ObjectMeta { func createPackage(client *client.Client, fnName, envName, srcArchiveName, deployArchiveName, buildcmd string) *metav1.ObjectMeta {
pkgSpec := fission.PackageSpec{ pkgSpec := fission.PackageSpec{
Environment: fission.EnvironmentReference{ Environment: fission.EnvironmentReference{
Namespace: metav1.NamespaceDefault, Namespace: metav1.NamespaceDefault,
Name: envName, Name: envName,
}, },
} }
var pkgStatus fission.BuildStatus = fission.BuildStatusSucceeded
var pkgStatus fission.BuildStatus
if len(deployArchiveName) > 0 { if len(deployArchiveName) > 0 {
pkgSpec.Deployment = *createArchive(client, deployArchiveName) pkgSpec.Deployment = *createArchive(client, deployArchiveName)
if len(srcArchiveName) > 0 {
fmt.Println("Deployment may be overwritten by builder manager after source package compilation")
}
} }
if len(srcArchiveName) > 0 { if len(srcArchiveName) > 0 {
pkgSpec.Source = *createArchive(client, srcArchiveName) pkgSpec.Source = *createArchive(client, srcArchiveName)
// set pending status to package }
// start a build only when a package has no deploy archive
if len(srcArchiveName) > 0 && len(deployArchiveName) == 0 {
pkgStatus = fission.BuildStatusPending pkgStatus = fission.BuildStatusPending
} else {
pkgStatus = fission.BuildStatusNone
} }
if len(buildcmd) > 0 { if len(buildcmd) > 0 {
pkgSpec.BuildCommand = buildcmd pkgSpec.BuildCommand = buildcmd
} }
pkgName := strings.ToLower(uuid.NewV4().String()) fnList, err := json.Marshal([]string{fnName})
checkErr(err, "encode json")
annotation := map[string]string{
"createdForFunction": fnName,
"usedByFunctions": string(fnList),
}
pkgName := strings.ToLower(fmt.Sprintf("%v-%v", fnName, uniuri.NewLen(6)))
pkg := &crd.Package{ pkg := &crd.Package{
Metadata: metav1.ObjectMeta{ Metadata: metav1.ObjectMeta{
Name: pkgName, Name: pkgName,
Namespace: metav1.NamespaceDefault, Namespace: metav1.NamespaceDefault,
Annotations: annotation,
}, },
Spec: pkgSpec, Spec: pkgSpec,
Status: fission.PackageStatus{ Status: fission.PackageStatus{
@@ -194,11 +208,8 @@ func fnCreate(c *cli.Context) error {
entrypoint := c.String("entrypoint") entrypoint := c.String("entrypoint")
buildcmd := c.String("buildcmd") buildcmd := c.String("buildcmd")
if len(buildcmd) == 0 {
buildcmd = "/builder"
}
pkgMetadata := createPackage(client, envName, srcArchiveName, deployArchiveName, buildcmd) pkgMetadata := createPackage(client, fnName, envName, srcArchiveName, deployArchiveName, buildcmd)
function := &crd.Function{ function := &crd.Function{
Metadata: metav1.ObjectMeta{ Metadata: metav1.ObjectMeta{
@@ -357,7 +368,7 @@ func fnUpdate(c *cli.Context) error {
if len(deployArchiveName) > 0 || len(srcArchiveName) > 0 { if len(deployArchiveName) > 0 || len(srcArchiveName) > 0 {
// create a new package for function // create a new package for function
pkgMetadata := createPackage(client, pkgMetadata := createPackage(client, function.Metadata.Name,
function.Spec.Environment.Name, srcArchiveName, deployArchiveName, buildcmd) function.Spec.Environment.Name, srcArchiveName, deployArchiveName, buildcmd)
// update function spec with resource version // update function spec with resource version
+10 -4
View File
@@ -68,7 +68,7 @@ type (
instanceId string // poolmgr instance id instanceId string // poolmgr instance id
labelsForPool map[string]string labelsForPool map[string]string
requestChannel chan *choosePodRequest requestChannel chan *choosePodRequest
sharedMountPath string sharedMountPath string // used by generic pool when creating env deployment to specify the share volume path for fetcher & env
} }
// serialize the choosing of pods so that choices don't conflict // serialize the choosing of pods so that choices don't conflict
@@ -133,7 +133,7 @@ func MakeGenericPool(
instanceId: instanceId, instanceId: instanceId,
fetcherImage: fetcherImage, fetcherImage: fetcherImage,
useSvc: false, // defaults off -- svc takes a second or more to become routable, slowing cold start useSvc: false, // defaults off -- svc takes a second or more to become routable, slowing cold start
sharedMountPath: "/userfunc", // used by generic pool when creating env deployment to specify the share volume path for fetcher & env sharedMountPath: "/userfunc", // change this may break v1 compatibility, since most of the v1 environments have hard-coded "/userfunc" in loading path
} }
gp.runtimeImagePullPolicy = getImagePullPolicy(runtimeImagePullPolicy) gp.runtimeImagePullPolicy = getImagePullPolicy(runtimeImagePullPolicy)
@@ -328,7 +328,13 @@ func (gp *GenericPool) specializePod(pod *apiv1.Pod, metadata *metav1.ObjectMeta
return err return err
} }
// for backward compatibility, since most v1 env
// still try to load user function from hard coded
// path /userfunc/user
targetFilename := "user" targetFilename := "user"
if gp.env.Spec.Version == 2 {
targetFilename = string(fn.Metadata.UID)
}
err = fetcherClient.MakeClient(fetcherUrl).Fetch(&fetcher.FetchRequest{ err = fetcherClient.MakeClient(fetcherUrl).Fetch(&fetcher.FetchRequest{
FetchType: fetcher.FETCH_DEPLOYMENT, FetchType: fetcher.FETCH_DEPLOYMENT,
@@ -336,7 +342,7 @@ func (gp *GenericPool) specializePod(pod *apiv1.Pod, metadata *metav1.ObjectMeta
Namespace: fn.Spec.Package.PackageRef.Namespace, Namespace: fn.Spec.Package.PackageRef.Namespace,
Name: fn.Spec.Package.PackageRef.Name, Name: fn.Spec.Package.PackageRef.Name,
}, },
Filename: targetFilename, // XXX use function id instead Filename: targetFilename,
}) })
if err != nil { if err != nil {
return err return err
@@ -433,7 +439,7 @@ func (gp *GenericPool) createPool() error {
{ {
Name: gp.env.Metadata.Name, Name: gp.env.Metadata.Name,
Image: gp.env.Spec.Runtime.Image, Image: gp.env.Spec.Runtime.Image,
ImagePullPolicy: apiv1.PullIfNotPresent, ImagePullPolicy: gp.runtimeImagePullPolicy,
TerminationMessagePath: "/dev/termination-log", TerminationMessagePath: "/dev/termination-log",
VolumeMounts: []apiv1.VolumeMount{ VolumeMounts: []apiv1.VolumeMount{
{ {
+9 -6
View File
@@ -10,19 +10,22 @@ fi
source $(dirname $0)/test_utils.sh source $(dirname $0)/test_utils.sh
IMAGE=gcr.io/fission-ci/fission-bundle REPO=gcr.io/fission-ci
FETCHER_IMAGE=gcr.io/fission-ci/fetcher IMAGE=$REPO/fission-bundle
PYTHON_RUNTIME_IMAGE=gcr.io/fission-ci/python-env FETCHER_IMAGE=$REPO/fetcher
PYTHON_BUILDER_IMAGE=gcr.io/fission-ci/python-env-builder
TAG=test TAG=test
build_and_push_fission_bundle $IMAGE:$TAG build_and_push_fission_bundle $IMAGE:$TAG
build_and_push_fetcher $FETCHER_IMAGE:$TAG build_and_push_fetcher $FETCHER_IMAGE:$TAG
build_and_push_python_env_runtime $PYTHON_RUNTIME_IMAGE:$TAG build_builder
build_and_push_python_env_builder $PYTHON_BUILDER_IMAGE:$TAG ENV='python3'
build_and_push_env_runtime $ENV $REPO/$ENV-env:$TAG
build_and_push_env_builder $ENV $REPO/$ENV-env-builder:$TAG
build_fission_cli build_fission_cli
+15 -10
View File
@@ -50,10 +50,17 @@ build_and_push_fetcher() {
popd popd
} }
build_and_push_python_env_runtime() { build_builder() {
image_tag=$1 pushd $ROOT/builder/cmd
./build.sh
popd
}
pushd $ROOT/environments/python3/ build_and_push_env_runtime() {
env=$1
image_tag=$2
pushd $ROOT/environments/$env/
docker build -t $image_tag . docker build -t $image_tag .
gcloud_login gcloud_login
@@ -62,13 +69,11 @@ build_and_push_python_env_runtime() {
popd popd
} }
build_and_push_python_env_builder() { build_and_push_env_builder() {
image_tag=$1 env=$1
image_tag=$2
pushd $ROOT/builder/cmd pushd $ROOT/environments/$env/builder
./build.sh
popd
pushd $ROOT/environments/python3/builder
builderDir=${GOPATH}/src/github.com/fission/fission/builder/cmd builderDir=${GOPATH}/src/github.com/fission/fission/builder/cmd
cp ${builderDir}/builder . cp ${builderDir}/builder .
@@ -80,7 +85,6 @@ build_and_push_python_env_builder() {
popd popd
} }
build_fission_cli() { build_fission_cli() {
pushd $ROOT/fission pushd $ROOT/fission
go build . go build .
@@ -240,6 +244,7 @@ dump_logs() {
dump_fission_logs $ns $fns controller dump_fission_logs $ns $fns controller
dump_fission_logs $ns $fns router dump_fission_logs $ns $fns router
dump_fission_logs $ns $fns poolmgr dump_fission_logs $ns $fns poolmgr
dump_fission_logs $ns $fns buildermgr
dump_function_pod_logs $ns $fns dump_function_pod_logs $ns $fns
dump_fission_crds dump_fission_crds
} }
+37 -14
View File
@@ -9,8 +9,8 @@ set -euo pipefail
# 2. package watcher triggers the build if any changes to packages # 2. package watcher triggers the build if any changes to packages
ROOT=$(dirname $0)/../.. ROOT=$(dirname $0)/../..
PYTHON_RUNTIME_IMAGE=gcr.io/fission-ci/python-env:test PYTHON_RUNTIME_IMAGE=gcr.io/fission-ci/python3-env:test
PYTHON_BUILDER_IMAGE=gcr.io/fission-ci/python-env-builder:test PYTHON_BUILDER_IMAGE=gcr.io/fission-ci/python3-env-builder:test
fn=python-srcbuild-$(date +%s) fn=python-srcbuild-$(date +%s)
@@ -23,21 +23,47 @@ checkFunctionResponse() {
echo $response | grep -i "a: 1 b: {c: 3, d: 4}" echo $response | grep -i "a: 1 b: {c: 3, d: 4}"
} }
waitBuild() {
echo "Waiting for builder manager to finish the build"
while true; do
kubectl --namespace default get packages $1 -o jsonpath='{.status.buildstatus}'|grep succeeded
if [[ $? -eq 0 ]]; then
break
fi
done
}
export -f waitBuild
waitEnvBuilder() {
echo "Waiting for env builder to catch up"
while true; do
kubectl --namespace fission-builder get pod|grep python|grep Running
if [[ $? -eq 0 ]]; then
break
fi
done
sleep 10
}
export -f waitEnvBuilder
echo "Pre-test cleanup" echo "Pre-test cleanup"
fission env delete --name python || true fission env delete --name python || true
kubectl --namespace default get packages|grep -v NAME|awk '{print $1}'|xargs -I@ bash -c 'kubectl --namespace default delete packages @' || true
echo "Creating python env" echo "Creating python env"
fission env create --name python --image $PYTHON_RUNTIME_IMAGE --builder $PYTHON_BUILDER_IMAGE fission env create --name python --image $PYTHON_RUNTIME_IMAGE --builder $PYTHON_BUILDER_IMAGE
trap "fission env delete --name python" EXIT trap "fission env delete --name python" EXIT
echo "Waiting for env builder to catch up" timeout 180s bash -c waitEnvBuilder
sleep 30
echo "Creating source pacakage" echo "Creating source pacakage"
zip -jr demo-src-pkg.zip $ROOT/examples/python/sourcepkg/ zip -jr demo-src-pkg.zip $ROOT/examples/python/sourcepkg/
echo "Creating function " $fn echo "Creating function " $fn
fission fn create --name $fn --env python --src demo-src-pkg.zip --entrypoint "main" --buildcmd "./build.sh" fission fn create --name $fn --env python --src demo-src-pkg.zip --entrypoint "user.main" --buildcmd "./build.sh"
trap "fission fn delete --name $fn" EXIT trap "fission fn delete --name $fn" EXIT
echo "Creating route" echo "Creating route"
@@ -46,15 +72,10 @@ fission route create --function $fn --url /$fn --method GET
echo "Waiting for router to catch up" echo "Waiting for router to catch up"
sleep 3 sleep 3
echo "Doing an HTTP POST on the builder manager's route to start a build"
pkg=$(kubectl --namespace default get functions $fn -o jsonpath='{.spec.package.packageref.name}') pkg=$(kubectl --namespace default get functions $fn -o jsonpath='{.spec.package.packageref.name}')
echo $pkg
response=$(curl -X POST $FISSION_URL/proxy/buildermgr/v1/build \
-H 'content-type: application/json' \
-d "{\"package\": {\"namespace\": \"default\",\"name\": \"$pkg\"}}")
echo "Waiting for builder manager to finish the build triggered by http request" # wait for build to finish at most 60s
sleep 30 timeout 60s bash -c "waitBuild $pkg"
checkFunctionResponse $fn checkFunctionResponse $fn
@@ -62,8 +83,10 @@ echo "Updating function " $fn
fission fn update --name $fn --src demo-src-pkg.zip fission fn update --name $fn --src demo-src-pkg.zip
trap "fission fn delete --name $fn" EXIT trap "fission fn delete --name $fn" EXIT
echo "Waiting for builder manager to finish the build triggered by packageWatcher" pkg=$(kubectl --namespace default get functions $fn -o jsonpath='{.spec.package.packageref.name}')
sleep 30
# wait for build to finish at most 60s
timeout 60s bash -c "waitBuild $pkg"
checkFunctionResponse $fn checkFunctionResponse $fn
+1
View File
@@ -264,6 +264,7 @@ const (
BuildStatusRunning = "running" BuildStatusRunning = "running"
BuildStatusSucceeded = "succeeded" BuildStatusSucceeded = "succeeded"
BuildStatusFailed = "failed" BuildStatusFailed = "failed"
BuildStatusNone = "none"
) )
const ( const (