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  }