sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/moonraker/client.go (about)

     1  /*
     2  Copyright 2023 The Kubernetes Authors.
     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 moonraker
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"net/http"
    26  	"net/url"
    27  	"sync"
    28  	"time"
    29  
    30  	"github.com/sirupsen/logrus"
    31  
    32  	"k8s.io/apimachinery/pkg/util/wait"
    33  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    34  	"sigs.k8s.io/prow/pkg/config"
    35  	"sigs.k8s.io/prow/pkg/version"
    36  )
    37  
    38  // The the Client needs a Config agent client. Here we require that the Agent
    39  // type fits the prowConfigAgentClient interface, which requires a Config()
    40  // method to retrieve the current Config. Tests can use a fake Config agent
    41  // instead of the real one.
    42  var _ prowConfigAgentClient = (*config.Agent)(nil)
    43  
    44  type prowConfigAgentClient interface {
    45  	Config() *config.Config
    46  }
    47  
    48  type Client struct {
    49  	host        string
    50  	configAgent prowConfigAgentClient
    51  
    52  	sync.Mutex // protects below
    53  	httpClient *http.Client
    54  }
    55  
    56  func NewClient(host string, configAgent prowConfigAgentClient) (*Client, error) {
    57  	c := Client{
    58  		host: host,
    59  		httpClient: &http.Client{
    60  			Timeout: configAgent.Config().Moonraker.ClientTimeout.Duration,
    61  		},
    62  		configAgent: configAgent,
    63  	}
    64  
    65  	// isMoonrakerUp is a ConditionFunc (see
    66  	// https://pkg.go.dev/k8s.io/apimachinery/pkg/util/wait#ConditionFunc).
    67  	isMoonrakerUp := func() (bool, error) {
    68  		if err := c.Ping(); err != nil {
    69  			return false, nil
    70  		} else {
    71  			return true, nil
    72  		}
    73  	}
    74  
    75  	pollLoopTimeout := 15 * time.Second
    76  	pollInterval := 500 * time.Millisecond
    77  	if err := wait.Poll(pollInterval, pollLoopTimeout, isMoonrakerUp); err != nil {
    78  		return nil, errors.New("timed out waiting for Moonraker to be available")
    79  	}
    80  
    81  	return &c, nil
    82  }
    83  
    84  func (c *Client) do(method, path string, payload []byte, params map[string]string) (*http.Response, error) {
    85  	baseURL, err := url.JoinPath(c.host, path)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  
    90  	req, err := http.NewRequest(method, baseURL, bytes.NewBuffer(payload))
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  	req.Header.Add("Content-Type", "application/json; charset=UTF-8")
    95  	req.Header.Add("User-Agent", version.UserAgent())
    96  	q := req.URL.Query()
    97  	for key, val := range params {
    98  		q.Set(key, val)
    99  	}
   100  	req.URL.RawQuery = q.Encode()
   101  	return c.maybeUpdatedHttpClient().Do(req)
   102  }
   103  
   104  // maybeUpdatedHttpClient returns a new HTTP client with a new one (with
   105  // a new timeout) if the provided timeout does not match the value already used
   106  // for the current HTTP client.
   107  func (c *Client) maybeUpdatedHttpClient() *http.Client {
   108  	c.Lock()
   109  	defer c.Unlock()
   110  	timeout := c.configAgent.Config().Moonraker.ClientTimeout.Duration
   111  
   112  	if c.httpClient.Timeout != timeout {
   113  		c.httpClient = &http.Client{Timeout: timeout}
   114  	}
   115  
   116  	return c.httpClient
   117  }
   118  
   119  func (c *Client) Ping() error {
   120  	resp, err := c.do(http.MethodGet, PathPing, nil, nil)
   121  	if err != nil {
   122  		return err
   123  	}
   124  	if resp.StatusCode == 200 {
   125  		return nil
   126  	}
   127  
   128  	return fmt.Errorf("invalid status code %d", resp.StatusCode)
   129  }
   130  
   131  // GetProwYAML returns the inrepoconfig contents for a repo, based on the Refs
   132  // struct as the input. From the Refs, Moonraker can determine the org/repo,
   133  // BaseSHA, and the Pulls[] (additional refs of each PR, if any) to grab the
   134  // inrepoconfig contents.
   135  func (c *Client) GetProwYAML(refs *prowapi.Refs) (*config.ProwYAML, error) {
   136  	payload := payload{
   137  		Refs: *refs,
   138  	}
   139  	buf, err := json.Marshal(payload)
   140  	if err != nil {
   141  		return nil, fmt.Errorf("could not marshal %v", payload)
   142  	}
   143  
   144  	resp, err := c.do(http.MethodPost, PathGetInrepoconfig, buf, nil)
   145  	if err != nil {
   146  		return nil, err
   147  	}
   148  	if resp.StatusCode >= 300 {
   149  		return nil, fmt.Errorf("got %v response", resp.StatusCode)
   150  	}
   151  
   152  	body, err := io.ReadAll(resp.Body)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  
   157  	prowYAML := config.ProwYAML{}
   158  	err = json.Unmarshal(body, &prowYAML)
   159  	if err != nil {
   160  		logrus.WithError(err).Info("unable to unmarshal getInrepoconfig request")
   161  		return nil, err
   162  	}
   163  
   164  	return &prowYAML, nil
   165  }
   166  
   167  // GetInRepoConfig just wraps around GetProwYAML(), converting the input
   168  // parameters into a prowapi.Refs{} type.
   169  //
   170  // Importantly, it also does defaulting of the retrieved jobs. Defaulting is
   171  // required because the Presubmit and Postsubmit job types have private fields
   172  // in them that would not be serialized into JSON when sent over from the
   173  // server. So the defaulting has to be done client-side.
   174  func (c *Client) GetInRepoConfig(identifier, baseBranch string, baseSHAGetter config.RefGetter, headSHAGetters ...config.RefGetter) (*config.ProwYAML, error) {
   175  	refs := prowapi.Refs{}
   176  
   177  	orgRepo := config.NewOrgRepo(identifier)
   178  	refs.Org = orgRepo.Org
   179  	refs.Repo = orgRepo.Repo
   180  
   181  	var err error
   182  	refs.BaseSHA, err = baseSHAGetter()
   183  	if err != nil {
   184  		return nil, err
   185  	}
   186  
   187  	refs.BaseRef = baseBranch
   188  
   189  	pulls := []prowapi.Pull{}
   190  	for _, headSHAGetter := range headSHAGetters {
   191  		headSHA, err := headSHAGetter()
   192  		if err != nil {
   193  			return nil, err
   194  		}
   195  		pulls = append(pulls, prowapi.Pull{
   196  			SHA: headSHA,
   197  		})
   198  	}
   199  	refs.Pulls = pulls
   200  
   201  	prowYAML, err := c.GetProwYAML(&refs)
   202  	if err != nil {
   203  		return nil, err
   204  	}
   205  
   206  	cfg := c.configAgent.Config()
   207  	if err := config.DefaultAndValidateProwYAML(cfg, prowYAML, identifier); err != nil {
   208  		return nil, err
   209  	}
   210  
   211  	return prowYAML, nil
   212  }
   213  
   214  func (c *Client) GetPresubmits(identifier, baseBranch string, baseSHAGetter config.RefGetter, headSHAGetters ...config.RefGetter) ([]config.Presubmit, error) {
   215  	prowYAML, err := c.GetInRepoConfig(identifier, baseBranch, baseSHAGetter, headSHAGetters...)
   216  	if err != nil {
   217  		return nil, err
   218  	}
   219  
   220  	config := c.configAgent.Config()
   221  	return append(config.GetPresubmitsStatic(identifier), prowYAML.Presubmits...), nil
   222  }
   223  
   224  func (c *Client) GetPostsubmits(identifier, baseBranch string, baseSHAGetter config.RefGetter, headSHAGetters ...config.RefGetter) ([]config.Postsubmit, error) {
   225  	prowYAML, err := c.GetInRepoConfig(identifier, baseBranch, baseSHAGetter, headSHAGetters...)
   226  	if err != nil {
   227  		return nil, err
   228  	}
   229  
   230  	config := c.configAgent.Config()
   231  	return append(config.GetPostsubmitsStatic(identifier), prowYAML.Postsubmits...), nil
   232  }