go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/led/ledcmd/edit_isolated.go (about) 1 // Copyright 2020 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package ledcmd 16 17 import ( 18 "context" 19 "io/ioutil" 20 "os" 21 "os/exec" 22 23 "github.com/bazelbuild/remote-apis-sdks/go/pkg/client" 24 "github.com/bazelbuild/remote-apis-sdks/go/pkg/command" 25 "github.com/bazelbuild/remote-apis-sdks/go/pkg/digest" 26 "github.com/bazelbuild/remote-apis-sdks/go/pkg/filemetadata" 27 28 "go.chromium.org/luci/auth" 29 "go.chromium.org/luci/client/casclient" 30 "go.chromium.org/luci/common/errors" 31 "go.chromium.org/luci/common/logging" 32 swarmingpb "go.chromium.org/luci/swarming/proto/api_v2" 33 34 "go.chromium.org/luci/led/job" 35 ) 36 37 // IsolatedTransformer is a function which receives a directory on the local 38 // disk with the contents of an isolate and is expected to manipulate the 39 // contents of that directory however it chooses. 40 // 41 // EditIsolated takes these functions as a callback in order to manipulate the 42 // isolated content of a job.Definition. 43 type IsolatedTransformer func(ctx context.Context, directory string) error 44 45 // ProgramIsolatedTransformer returns an IsolatedTransformer which alters the 46 // contents of the isolated by running a program specified with `args` in the 47 // directory where the isolated content has been unpacked. 48 func ProgramIsolatedTransformer(args ...string) IsolatedTransformer { 49 return func(ctx context.Context, dir string) error { 50 logging.Infof(ctx, "Invoking transform_program: %q", args) 51 tProg := exec.CommandContext(ctx, args[0], args[1:]...) 52 tProg.Stdout = os.Stderr 53 tProg.Stderr = os.Stderr 54 tProg.Dir = dir 55 return errors.Annotate(tProg.Run(), "running transform_program").Err() 56 } 57 } 58 59 // PromptIsolatedTransformer returns an IsolatedTransformer which prompts the 60 // user to navigate to the directory with the isolated content and manipulate 61 // it manually. When the user is done they should press "enter" to indicate that 62 // they're finished. 63 func PromptIsolatedTransformer() IsolatedTransformer { 64 return func(ctx context.Context, dir string) error { 65 logging.Infof(ctx, "") 66 logging.Infof(ctx, "Edit files as you wish in:") 67 logging.Infof(ctx, "\t%s", dir) 68 return awaitNewline(ctx) 69 } 70 } 71 72 // EditIsolated allows you to edit the isolated (cas_input_root) 73 // contents of the job.Definition. 74 // 75 // This implicitly collapses all isolated sources in the job.Definition into 76 // a single isolated source. 77 // The output job.Definition always has cas_user_payload. 78 func EditIsolated(ctx context.Context, authOpts auth.Options, jd *job.Definition, xform IsolatedTransformer) error { 79 logging.Infof(ctx, "editing isolated") 80 81 tdir, err := ioutil.TempDir("", "led-edit-isolated") 82 if err != nil { 83 return errors.Annotate(err, "failed to create tempdir").Err() 84 } 85 defer func() { 86 if err = os.RemoveAll(tdir); err != nil { 87 logging.Errorf(ctx, "failed to cleanup temp dir %q: %s", tdir, err) 88 } 89 }() 90 91 if err := ConsolidateRbeCasSources(ctx, authOpts, jd); err != nil { 92 return err 93 } 94 95 current, err := jd.Info().CurrentIsolated() 96 if err != nil { 97 return err 98 } 99 100 err = jd.Edit(func(je job.Editor) { 101 je.ClearCurrentIsolated() 102 }) 103 if err != nil { 104 return err 105 } 106 107 casClient, err := newCASClient(ctx, authOpts, jd) 108 if err != nil { 109 return err 110 } 111 defer casClient.Close() 112 113 if err = downloadFromCas(ctx, current, casClient, tdir); err != nil { 114 return err 115 } 116 117 if err := xform(ctx, tdir); err != nil { 118 return err 119 } 120 121 logging.Infof(ctx, "uploading new isolated to RBE-CAS") 122 casRef, err := uploadToCas(ctx, casClient, tdir) 123 if err != nil { 124 return errors.Annotate(err, "errors in uploadToCas").Err() 125 } 126 logging.Infof(ctx, "isolated upload: done") 127 if jd.GetSwarming() != nil { 128 jd.GetSwarming().CasUserPayload = casRef 129 return nil 130 } 131 132 return jd.HighLevelEdit(func(je job.HighLevelEditor) { 133 je.TaskPayloadSource("", "") 134 je.CASTaskPayload(job.RecipeDirectory, casRef) 135 }) 136 } 137 138 func getCASInstance(jd *job.Definition) (string, error) { 139 current, err := jd.Info().CurrentIsolated() 140 if err != nil { 141 return "", err 142 } 143 casInstance := current.GetCasInstance() 144 if casInstance == "" { 145 if casInstance, err = jd.CasInstance(); err != nil { 146 return "", err 147 } 148 } 149 return casInstance, nil 150 } 151 152 func newCASClient(ctx context.Context, authOpts auth.Options, jd *job.Definition) (*client.Client, error) { 153 casInstance, err := getCASInstance(jd) 154 if err != nil { 155 return nil, err 156 } 157 return casclient.NewLegacy(ctx, casclient.AddrProd, casInstance, authOpts, false) 158 } 159 160 func downloadFromCas(ctx context.Context, casRef *swarmingpb.CASReference, casClient *client.Client, tdir string) error { 161 if casRef.GetDigest().GetHash() == "" { 162 return nil 163 } 164 d := digest.Digest{ 165 Hash: casRef.Digest.Hash, 166 Size: casRef.Digest.SizeBytes, 167 } 168 logging.Infof(ctx, "downloading from RBE-CAS...") 169 _, _, err := casClient.DownloadDirectory(ctx, d, tdir, filemetadata.NewNoopCache()) 170 if err != nil { 171 return errors.Annotate(err, "failed to download directory").Err() 172 } 173 return nil 174 } 175 176 func uploadToCas(ctx context.Context, client *client.Client, dir string) (*swarmingpb.CASReference, error) { 177 is := command.InputSpec{ 178 Inputs: []string{"."}, // entire dir 179 } 180 rootDg, entries, _, err := client.ComputeMerkleTree(ctx, dir, "", "", &is, filemetadata.NewNoopCache()) 181 if err != nil { 182 return nil, errors.Annotate(err, "failed to compute Merkle Tree").Err() 183 } 184 185 _, _, err = client.UploadIfMissing(ctx, entries...) 186 if err != nil { 187 return nil, errors.Annotate(err, "failed to upload items").Err() 188 } 189 return &swarmingpb.CASReference{ 190 CasInstance: client.InstanceName, 191 Digest: &swarmingpb.Digest{ 192 Hash: rootDg.Hash, 193 SizeBytes: rootDg.Size, 194 }, 195 }, nil 196 }