kythe.io@v0.0.68-0.20240422202219-7225dbc01741/kythe/go/extractors/gcp/config/converter.go (about)

     1  /*
     2   * Copyright 2018 The Kythe Authors. All rights reserved.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *   http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  // Package config contains logic for converting
    18  // kythe.proto.extraction.RepoConfig to cloudbuild.yaml format as specified by
    19  // https://cloud.google.com/cloud-build/docs/build-config.
    20  package config // import "kythe.io/kythe/go/extractors/gcp/config"
    21  
    22  import (
    23  	"fmt"
    24  	"io/ioutil"
    25  	"os"
    26  	"path"
    27  	"strconv"
    28  	"strings"
    29  
    30  	rpb "kythe.io/kythe/proto/repo_go_proto"
    31  
    32  	"google.golang.org/api/cloudbuild/v1"
    33  	"google.golang.org/protobuf/encoding/protojson"
    34  	"sigs.k8s.io/yaml"
    35  )
    36  
    37  // Constants that map input/output substitutions.
    38  const (
    39  	defaultCorpus   = "${_CORPUS}"
    40  	defaultVersion  = "${_COMMIT}"
    41  	outputGsBucket  = "${_BUCKET_NAME}"
    42  	defaultRepoName = "${_REPO}"
    43  )
    44  
    45  // Constants ephemeral to a single kythe cloudbuild run.
    46  const (
    47  	outputDirectory = "/workspace/output"
    48  	codeDirectory   = "/workspace/code"
    49  	javaVolumeName  = "kythe_extractors"
    50  )
    51  
    52  // KytheToYAML takes an input JSON file defined in
    53  // kythe.proto.extraction.RepoConfig format, and returns it as marshalled YAML.
    54  func KytheToYAML(input string) ([]byte, error) {
    55  	configProto, err := readConfigFile(input)
    56  	if err != nil {
    57  		return nil, fmt.Errorf("reading config file %s: %v", input, configProto)
    58  	}
    59  	build, err := KytheToBuild(configProto)
    60  	if err != nil {
    61  		return nil, fmt.Errorf("converting cloudbuild.Build: %v", err)
    62  	}
    63  	json, err := build.MarshalJSON()
    64  	if err != nil {
    65  		return nil, fmt.Errorf("marshalling cloudbuild.Build to JSON: %v", err)
    66  	}
    67  	return yaml.JSONToYAML(json)
    68  }
    69  
    70  // KytheToBuild takes a kythe.proto.extraction.RepoConfig and returns the
    71  // necessary cloudbuild.Build with BuildSteps for running kythe extraction.
    72  func KytheToBuild(conf *rpb.Config) (*cloudbuild.Build, error) {
    73  	if len(conf.Extractions) == 0 {
    74  		return nil, fmt.Errorf("config has no extraction specified")
    75  	} else if len(conf.Extractions) > 1 {
    76  		return nil, fmt.Errorf("we don't yet support multiple corpus extraction yet")
    77  	}
    78  
    79  	repo := conf.Repo
    80  	if repo == "" {
    81  		// Default to a $_{REPO} string replacement.
    82  		// TODO(danielmoy): this kludge should be removed in favor of having
    83  		// config generation be a separate process (something should generate a
    84  		// qualified rpb.Config object, obviating the need to fall back to the
    85  		// $_{VAR} replacements.
    86  		repo = defaultRepoName
    87  	}
    88  
    89  	hints := conf.Extractions[0]
    90  	if hints.Corpus == "" {
    91  		// Default to a $_{CORPUS} string replacement.
    92  		hints.Corpus = defaultCorpus
    93  	}
    94  
    95  	build := &cloudbuild.Build{
    96  		Artifacts: &cloudbuild.Artifacts{
    97  			Objects: &cloudbuild.ArtifactObjects{
    98  				Location: fmt.Sprintf("gs://%s/%s/", outputGsBucket, hints.Corpus),
    99  				Paths:    []string{path.Join(outputDirectory, outputFileName())},
   100  			},
   101  		},
   102  		// TODO(danielmoy): this should probably also be a generator, or at least
   103  		// if there is refactoring work done as described below to make steps
   104  		// more granular, this will have to hook into that logic.
   105  		Steps: commonSteps(repo),
   106  		Tags:  []string{hints.Corpus},
   107  	}
   108  
   109  	g, err := generator(hints.BuildSystem)
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  	build.Tags = append(build.Tags, "kythe_extract_"+strings.ToLower(hints.BuildSystem.String()))
   114  
   115  	build.Steps = append(build.Steps, g.preExtractSteps()...)
   116  
   117  	targets := hints.Targets
   118  	if len(targets) == 0 {
   119  		targets = append(targets, g.defaultExtractionTarget())
   120  	}
   121  	for i, target := range targets {
   122  		idSuffix := ""
   123  		if len(targets) > 1 {
   124  			idSuffix = strconv.Itoa(i)
   125  		}
   126  		build.Steps = append(build.Steps, g.extractSteps(hints.Corpus, target, idSuffix)...)
   127  	}
   128  
   129  	build.Steps = append(build.Steps, g.postExtractSteps(hints.Corpus)...)
   130  
   131  	return build, nil
   132  }
   133  
   134  func readConfigFile(input string) (*rpb.Config, error) {
   135  	conf := &rpb.Config{}
   136  	file, err := os.Open(input)
   137  	if err != nil {
   138  		return nil, fmt.Errorf("opening input file %s: %v", input, err)
   139  	}
   140  	defer file.Close()
   141  	rec, err := ioutil.ReadAll(file)
   142  	if err != nil {
   143  		return nil, fmt.Errorf("reading json file %s: %v", input, err)
   144  	}
   145  	if err := protojson.Unmarshal(rec, conf); err != nil {
   146  		return nil, fmt.Errorf("parsing json file %s: %v", input, err)
   147  	}
   148  	return conf, nil
   149  }
   150  
   151  // buildSystemElaborator encapsulates the logic for adding extra build steps for a
   152  // specific type of build system (maven, gradle, etc).
   153  // TODO(danielmoy): we will almost certainly need to support more fine-grained
   154  // steps and/or artifacts.  For example now artifacts are prepended, we might
   155  // need ones that are appended.  We might need build steps that occur before or
   156  // directly after cloning, but before other common steps.
   157  type buildSystemElaborator interface {
   158  	// preExtractSteps is a list of cloudbuild steps to be done before
   159  	// extraction starts, specific to this extractor type.
   160  	preExtractSteps() []*cloudbuild.BuildStep
   161  	// extractSteps is a list of cloudbuild steps to extract the given target.
   162  	// The idSuffix is simply a unique identifier for this instance and target,
   163  	// for use in coordinating paralleism if desired.
   164  	extractSteps(corpus string, target *rpb.ExtractionTarget, idSuffix string) []*cloudbuild.BuildStep
   165  	// postExtractSteps is a list of cloudbuild steps to be done after
   166  	// extraction finishes.
   167  	postExtractSteps(corpus string) []*cloudbuild.BuildStep
   168  	// defaultExtractionTarget is the repo-relative location of the default build
   169  	// configuration file for a repo of a given type.  For example for a bazel
   170  	// repo it might just be root/BUILD, or a maven repo will have repo/pom.xml.
   171  	defaultExtractionTarget() *rpb.ExtractionTarget
   172  }
   173  
   174  func generator(b rpb.BuildSystem) (buildSystemElaborator, error) {
   175  	switch b {
   176  	case rpb.BuildSystem_MAVEN:
   177  		return &mavenGenerator{}, nil
   178  	case rpb.BuildSystem_GRADLE:
   179  		return &gradleGenerator{}, nil
   180  	case rpb.BuildSystem_BAZEL:
   181  		return &bazelGenerator{}, nil
   182  	default:
   183  		return nil, fmt.Errorf("unsupported build system %s", b)
   184  	}
   185  }
   186  
   187  func outputFileName() string {
   188  	return defaultVersion + ".kzip"
   189  }