mirror of
https://github.com/actions/actions-runner-controller.git
synced 2025-12-10 11:41:27 +00:00
Introduce new preview auto-scaling mode for ARC. (#2153)
Co-authored-by: Cory Miller <cory-miller@github.com> Co-authored-by: Nikola Jokic <nikola-jokic@github.com> Co-authored-by: Ava Stancu <AvaStancu@github.com> Co-authored-by: Ferenc Hammerl <fhammerl@github.com> Co-authored-by: Francesco Renzi <rentziass@github.com> Co-authored-by: Bassem Dghaidi <Link-@github.com>
This commit is contained in:
@@ -37,6 +37,7 @@ RUN --mount=target=. \
|
|||||||
--mount=type=cache,mode=0777,target=${GOCACHE} \
|
--mount=type=cache,mode=0777,target=${GOCACHE} \
|
||||||
export GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} && \
|
export GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} && \
|
||||||
go build -trimpath -ldflags="-s -w -X 'github.com/actions/actions-runner-controller/build.Version=${VERSION}'" -o /out/manager main.go && \
|
go build -trimpath -ldflags="-s -w -X 'github.com/actions/actions-runner-controller/build.Version=${VERSION}'" -o /out/manager main.go && \
|
||||||
|
go build -trimpath -ldflags="-s -w" -o /out/github-runnerscaleset-listener ./cmd/githubrunnerscalesetlistener && \
|
||||||
go build -trimpath -ldflags="-s -w" -o /out/github-webhook-server ./cmd/githubwebhookserver && \
|
go build -trimpath -ldflags="-s -w" -o /out/github-webhook-server ./cmd/githubwebhookserver && \
|
||||||
go build -trimpath -ldflags="-s -w" -o /out/actions-metrics-server ./cmd/actionsmetricsserver
|
go build -trimpath -ldflags="-s -w" -o /out/actions-metrics-server ./cmd/actionsmetricsserver
|
||||||
|
|
||||||
@@ -49,6 +50,7 @@ WORKDIR /
|
|||||||
COPY --from=builder /out/manager .
|
COPY --from=builder /out/manager .
|
||||||
COPY --from=builder /out/github-webhook-server .
|
COPY --from=builder /out/github-webhook-server .
|
||||||
COPY --from=builder /out/actions-metrics-server .
|
COPY --from=builder /out/actions-metrics-server .
|
||||||
|
COPY --from=builder /out/github-runnerscaleset-listener .
|
||||||
|
|
||||||
USER 65532:65532
|
USER 65532:65532
|
||||||
|
|
||||||
|
|||||||
5
Makefile
5
Makefile
@@ -86,6 +86,7 @@ test-with-deps: kube-apiserver etcd kubectl
|
|||||||
# Build manager binary
|
# Build manager binary
|
||||||
manager: generate fmt vet
|
manager: generate fmt vet
|
||||||
go build -o bin/manager main.go
|
go build -o bin/manager main.go
|
||||||
|
go build -o bin/github-runnerscaleset-listener ./cmd/githubrunnerscalesetlistener
|
||||||
|
|
||||||
# Run against the configured Kubernetes cluster in ~/.kube/config
|
# Run against the configured Kubernetes cluster in ~/.kube/config
|
||||||
run: generate fmt vet manifests
|
run: generate fmt vet manifests
|
||||||
@@ -115,6 +116,10 @@ manifests-gen-crds: controller-gen yq
|
|||||||
|
|
||||||
chart-crds:
|
chart-crds:
|
||||||
cp config/crd/bases/*.yaml charts/actions-runner-controller/crds/
|
cp config/crd/bases/*.yaml charts/actions-runner-controller/crds/
|
||||||
|
rm charts/actions-runner-controller/crds/actions.github.com_autoscalingrunnersets.yaml
|
||||||
|
rm charts/actions-runner-controller/crds/actions.github.com_autoscalinglisteners.yaml
|
||||||
|
rm charts/actions-runner-controller/crds/actions.github.com_ephemeralrunnersets.yaml
|
||||||
|
rm charts/actions-runner-controller/crds/actions.github.com_ephemeralrunners.yaml
|
||||||
|
|
||||||
# Run go fmt against code
|
# Run go fmt against code
|
||||||
fmt:
|
fmt:
|
||||||
|
|||||||
12
PROJECT
12
PROJECT
@@ -10,4 +10,16 @@ resources:
|
|||||||
- group: actions
|
- group: actions
|
||||||
kind: RunnerDeployment
|
kind: RunnerDeployment
|
||||||
version: v1alpha1
|
version: v1alpha1
|
||||||
|
- group: actions
|
||||||
|
kind: AutoscalingRunnerSet
|
||||||
|
version: v1alpha1
|
||||||
|
- group: actions
|
||||||
|
kind: EphemeralRunnerSet
|
||||||
|
version: v1alpha1
|
||||||
|
- group: actions
|
||||||
|
kind: EphemeralRunner
|
||||||
|
version: v1alpha1
|
||||||
|
- group: actions
|
||||||
|
kind: AutoscalingListener
|
||||||
|
version: v1alpha1
|
||||||
version: "2"
|
version: "2"
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 The actions-runner-controller authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AutoscalingListenerSpec defines the desired state of AutoscalingListener
|
||||||
|
type AutoscalingListenerSpec struct {
|
||||||
|
// Required
|
||||||
|
GitHubConfigUrl string `json:"githubConfigUrl,omitempty"`
|
||||||
|
|
||||||
|
// Required
|
||||||
|
GitHubConfigSecret string `json:"githubConfigSecret,omitempty"`
|
||||||
|
|
||||||
|
// Required
|
||||||
|
RunnerScaleSetId int `json:"runnerScaleSetId,omitempty"`
|
||||||
|
|
||||||
|
// Required
|
||||||
|
AutoscalingRunnerSetNamespace string `json:"autoscalingRunnerSetNamespace,omitempty"`
|
||||||
|
|
||||||
|
// Required
|
||||||
|
AutoscalingRunnerSetName string `json:"autoscalingRunnerSetName,omitempty"`
|
||||||
|
|
||||||
|
// Required
|
||||||
|
EphemeralRunnerSetName string `json:"ephemeralRunnerSetName,omitempty"`
|
||||||
|
|
||||||
|
// Required
|
||||||
|
// +kubebuilder:validation:Minimum:=0
|
||||||
|
MaxRunners int `json:"maxRunners,omitempty"`
|
||||||
|
|
||||||
|
// Required
|
||||||
|
// +kubebuilder:validation:Minimum:=0
|
||||||
|
MinRunners int `json:"minRunners,omitempty"`
|
||||||
|
|
||||||
|
// Required
|
||||||
|
Image string `json:"image,omitempty"`
|
||||||
|
|
||||||
|
// Required
|
||||||
|
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoscalingListenerStatus defines the observed state of AutoscalingListener
|
||||||
|
type AutoscalingListenerStatus struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
//+kubebuilder:object:root=true
|
||||||
|
//+kubebuilder:subresource:status
|
||||||
|
//+kubebuilder:printcolumn:JSONPath=".spec.githubConfigUrl",name=GitHub Configure URL,type=string
|
||||||
|
//+kubebuilder:printcolumn:JSONPath=".spec.autoscalingRunnerSetNamespace",name=AutoscalingRunnerSet Namespace,type=string
|
||||||
|
//+kubebuilder:printcolumn:JSONPath=".spec.autoscalingRunnerSetName",name=AutoscalingRunnerSet Name,type=string
|
||||||
|
|
||||||
|
// AutoscalingListener is the Schema for the autoscalinglisteners API
|
||||||
|
type AutoscalingListener struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||||
|
|
||||||
|
Spec AutoscalingListenerSpec `json:"spec,omitempty"`
|
||||||
|
Status AutoscalingListenerStatus `json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//+kubebuilder:object:root=true
|
||||||
|
|
||||||
|
// AutoscalingListenerList contains a list of AutoscalingListener
|
||||||
|
type AutoscalingListenerList struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ListMeta `json:"metadata,omitempty"`
|
||||||
|
Items []AutoscalingListener `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SchemeBuilder.Register(&AutoscalingListener{}, &AutoscalingListenerList{})
|
||||||
|
}
|
||||||
143
apis/actions.github.com/v1alpha1/autoscalingrunnerset_types.go
Normal file
143
apis/actions.github.com/v1alpha1/autoscalingrunnerset_types.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 The actions-runner-controller authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/actions/actions-runner-controller/hash"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
|
||||||
|
|
||||||
|
//+kubebuilder:object:root=true
|
||||||
|
//+kubebuilder:subresource:status
|
||||||
|
//+kubebuilder:printcolumn:JSONPath=".spec.minRunners",name=Minimum Runners,type=number
|
||||||
|
//+kubebuilder:printcolumn:JSONPath=".spec.maxRunners",name=Maximum Runners,type=number
|
||||||
|
//+kubebuilder:printcolumn:JSONPath=".status.currentRunners",name=Current Runners,type=number
|
||||||
|
//+kubebuilder:printcolumn:JSONPath=".status.state",name=State,type=string
|
||||||
|
|
||||||
|
// AutoscalingRunnerSet is the Schema for the autoscalingrunnersets API
|
||||||
|
type AutoscalingRunnerSet struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||||
|
|
||||||
|
Spec AutoscalingRunnerSetSpec `json:"spec,omitempty"`
|
||||||
|
Status AutoscalingRunnerSetStatus `json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoscalingRunnerSetSpec defines the desired state of AutoscalingRunnerSet
|
||||||
|
type AutoscalingRunnerSetSpec struct {
|
||||||
|
// Required
|
||||||
|
GitHubConfigUrl string `json:"githubConfigUrl,omitempty"`
|
||||||
|
|
||||||
|
// Required
|
||||||
|
GitHubConfigSecret string `json:"githubConfigSecret,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
RunnerGroup string `json:"runnerGroup,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
Proxy *ProxyConfig `json:"proxy,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
GitHubServerTLS *GitHubServerTLSConfig `json:"githubServerTLS,omitempty"`
|
||||||
|
|
||||||
|
// Required
|
||||||
|
Template corev1.PodTemplateSpec `json:"template,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
// +kubebuilder:validation:Minimum:=0
|
||||||
|
MaxRunners *int `json:"maxRunners,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
// +kubebuilder:validation:Minimum:=0
|
||||||
|
MinRunners *int `json:"minRunners,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GitHubServerTLSConfig struct {
|
||||||
|
// Required
|
||||||
|
RootCAsConfigMapRef string `json:"certConfigMapRef,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProxyConfig struct {
|
||||||
|
// +optional
|
||||||
|
HTTP *ProxyServerConfig `json:"http,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
HTTPS *ProxyServerConfig `json:"https,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProxyServerConfig struct {
|
||||||
|
// Required
|
||||||
|
Url string `json:"url,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
CredentialSecretRef string `json:"credentialSecretRef,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
NoProxy []string `json:"noProxy,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoscalingRunnerSetStatus defines the observed state of AutoscalingRunnerSet
|
||||||
|
type AutoscalingRunnerSetStatus struct {
|
||||||
|
// +optional
|
||||||
|
CurrentRunners int `json:"currentRunners,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
State string `json:"state,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ars *AutoscalingRunnerSet) ListenerSpecHash() string {
|
||||||
|
type listenerSpec = AutoscalingRunnerSetSpec
|
||||||
|
arsSpec := ars.Spec.DeepCopy()
|
||||||
|
spec := arsSpec
|
||||||
|
return hash.ComputeTemplateHash(&spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ars *AutoscalingRunnerSet) RunnerSetSpecHash() string {
|
||||||
|
type runnerSetSpec struct {
|
||||||
|
GitHubConfigUrl string
|
||||||
|
GitHubConfigSecret string
|
||||||
|
RunnerGroup string
|
||||||
|
Proxy *ProxyConfig
|
||||||
|
GitHubServerTLS *GitHubServerTLSConfig
|
||||||
|
Template corev1.PodTemplateSpec
|
||||||
|
}
|
||||||
|
spec := &runnerSetSpec{
|
||||||
|
GitHubConfigUrl: ars.Spec.GitHubConfigUrl,
|
||||||
|
GitHubConfigSecret: ars.Spec.GitHubConfigSecret,
|
||||||
|
RunnerGroup: ars.Spec.RunnerGroup,
|
||||||
|
Proxy: ars.Spec.Proxy,
|
||||||
|
GitHubServerTLS: ars.Spec.GitHubServerTLS,
|
||||||
|
Template: ars.Spec.Template,
|
||||||
|
}
|
||||||
|
return hash.ComputeTemplateHash(&spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
//+kubebuilder:object:root=true
|
||||||
|
|
||||||
|
// AutoscalingRunnerSetList contains a list of AutoscalingRunnerSet
|
||||||
|
type AutoscalingRunnerSetList struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ListMeta `json:"metadata,omitempty"`
|
||||||
|
Items []AutoscalingRunnerSet `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SchemeBuilder.Register(&AutoscalingRunnerSet{}, &AutoscalingRunnerSetList{})
|
||||||
|
}
|
||||||
130
apis/actions.github.com/v1alpha1/ephemeralrunner_types.go
Normal file
130
apis/actions.github.com/v1alpha1/ephemeralrunner_types.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 The actions-runner-controller authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
//+kubebuilder:object:root=true
|
||||||
|
//+kubebuilder:subresource:status
|
||||||
|
// +kubebuilder:printcolumn:JSONPath=".spec.githubConfigUrl",name="GitHub Config URL",type=string
|
||||||
|
// +kubebuilder:printcolumn:JSONPath=".status.runnerId",name=RunnerId,type=number
|
||||||
|
// +kubebuilder:printcolumn:JSONPath=".status.phase",name=Status,type=string
|
||||||
|
// +kubebuilder:printcolumn:JSONPath=".status.jobRepositoryName",name=JobRepository,type=string
|
||||||
|
// +kubebuilder:printcolumn:JSONPath=".status.jobWorkflowRef",name=JobWorkflowRef,type=string
|
||||||
|
// +kubebuilder:printcolumn:JSONPath=".status.workflowRunId",name=WorkflowRunId,type=number
|
||||||
|
// +kubebuilder:printcolumn:JSONPath=".status.jobDisplayName",name=JobDisplayName,type=string
|
||||||
|
// +kubebuilder:printcolumn:JSONPath=".status.message",name=Message,type=string
|
||||||
|
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
|
||||||
|
|
||||||
|
// EphemeralRunner is the Schema for the ephemeralrunners API
|
||||||
|
type EphemeralRunner struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||||
|
|
||||||
|
Spec EphemeralRunnerSpec `json:"spec,omitempty"`
|
||||||
|
Status EphemeralRunnerStatus `json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EphemeralRunnerSpec defines the desired state of EphemeralRunner
|
||||||
|
type EphemeralRunnerSpec struct {
|
||||||
|
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
|
||||||
|
// Important: Run "make" to regenerate code after modifying this file
|
||||||
|
|
||||||
|
// +required
|
||||||
|
GitHubConfigUrl string `json:"githubConfigUrl,omitempty"`
|
||||||
|
|
||||||
|
// +required
|
||||||
|
GitHubConfigSecret string `json:"githubConfigSecret,omitempty"`
|
||||||
|
|
||||||
|
// +required
|
||||||
|
RunnerScaleSetId int `json:"runnerScaleSetId,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
Proxy *ProxyConfig `json:"proxy,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
GitHubServerTLS *GitHubServerTLSConfig `json:"githubServerTLS,omitempty"`
|
||||||
|
|
||||||
|
// +required
|
||||||
|
corev1.PodTemplateSpec `json:",inline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EphemeralRunnerStatus defines the observed state of EphemeralRunner
|
||||||
|
type EphemeralRunnerStatus struct {
|
||||||
|
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
|
||||||
|
// Important: Run "make" to regenerate code after modifying this file
|
||||||
|
|
||||||
|
// Turns true only if the runner is online.
|
||||||
|
// +optional
|
||||||
|
Ready bool `json:"ready"`
|
||||||
|
// Phase describes phases where EphemeralRunner can be in.
|
||||||
|
// The underlying type is a PodPhase, but the meaning is more restrictive
|
||||||
|
//
|
||||||
|
// The PodFailed phase should be set only when EphemeralRunner fails to start
|
||||||
|
// after multiple retries. That signals that this EphemeralRunner won't work,
|
||||||
|
// and manual inspection is required
|
||||||
|
//
|
||||||
|
// The PodSucceded phase should be set only when confirmed that EphemeralRunner
|
||||||
|
// actually executed the job and has been removed from the service.
|
||||||
|
// +optional
|
||||||
|
Phase corev1.PodPhase `json:"phase,omitempty"`
|
||||||
|
// +optional
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
// +optional
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
RunnerId int `json:"runnerId,omitempty"`
|
||||||
|
// +optional
|
||||||
|
RunnerName string `json:"runnerName,omitempty"`
|
||||||
|
// +optional
|
||||||
|
RunnerJITConfig string `json:"runnerJITConfig,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
Failures map[string]bool `json:"failures,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
JobRequestId int64 `json:"jobRequestId,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
JobRepositoryName string `json:"jobRepositoryName,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
JobWorkflowRef string `json:"jobWorkflowRef,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
WorkflowRunId int64 `json:"workflowRunId,omitempty"`
|
||||||
|
|
||||||
|
// +optional
|
||||||
|
JobDisplayName string `json:"jobDisplayName,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//+kubebuilder:object:root=true
|
||||||
|
|
||||||
|
// EphemeralRunnerList contains a list of EphemeralRunner
|
||||||
|
type EphemeralRunnerList struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ListMeta `json:"metadata,omitempty"`
|
||||||
|
Items []EphemeralRunner `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SchemeBuilder.Register(&EphemeralRunner{}, &EphemeralRunnerList{})
|
||||||
|
}
|
||||||
61
apis/actions.github.com/v1alpha1/ephemeralrunnerset_types.go
Normal file
61
apis/actions.github.com/v1alpha1/ephemeralrunnerset_types.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 The actions-runner-controller authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EphemeralRunnerSetSpec defines the desired state of EphemeralRunnerSet
|
||||||
|
type EphemeralRunnerSetSpec struct {
|
||||||
|
// Replicas is the number of desired EphemeralRunner resources in the k8s namespace.
|
||||||
|
Replicas int `json:"replicas,omitempty"`
|
||||||
|
|
||||||
|
EphemeralRunnerSpec EphemeralRunnerSpec `json:"ephemeralRunnerSpec,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EphemeralRunnerSetStatus defines the observed state of EphemeralRunnerSet
|
||||||
|
type EphemeralRunnerSetStatus struct {
|
||||||
|
// CurrentReplicas is the number of currently running EphemeralRunner resources being managed by this EphemeralRunnerSet.
|
||||||
|
CurrentReplicas int `json:"currentReplicas,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:object:root=true
|
||||||
|
// +kubebuilder:subresource:status
|
||||||
|
// +kubebuilder:printcolumn:JSONPath=".spec.replicas",name="DesiredReplicas",type="integer"
|
||||||
|
// +kubebuilder:printcolumn:JSONPath=".status.currentReplicas", name="CurrentReplicas",type="integer"
|
||||||
|
// EphemeralRunnerSet is the Schema for the ephemeralrunnersets API
|
||||||
|
type EphemeralRunnerSet struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||||
|
|
||||||
|
Spec EphemeralRunnerSetSpec `json:"spec,omitempty"`
|
||||||
|
Status EphemeralRunnerSetStatus `json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//+kubebuilder:object:root=true
|
||||||
|
|
||||||
|
// EphemeralRunnerSetList contains a list of EphemeralRunnerSet
|
||||||
|
type EphemeralRunnerSetList struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ListMeta `json:"metadata,omitempty"`
|
||||||
|
Items []EphemeralRunnerSet `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SchemeBuilder.Register(&EphemeralRunnerSet{}, &EphemeralRunnerSetList{})
|
||||||
|
}
|
||||||
36
apis/actions.github.com/v1alpha1/groupversion_info.go
Normal file
36
apis/actions.github.com/v1alpha1/groupversion_info.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 The actions-runner-controller authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package v1 contains API Schema definitions for the batch v1 API group
|
||||||
|
// +kubebuilder:object:generate=true
|
||||||
|
// +groupName=actions.github.com
|
||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/scheme"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// GroupVersion is group version used to register these objects
|
||||||
|
GroupVersion = schema.GroupVersion{Group: "actions.github.com", Version: "v1alpha1"}
|
||||||
|
|
||||||
|
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
|
||||||
|
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
|
||||||
|
|
||||||
|
// AddToScheme adds the types in this group-version to the given scheme.
|
||||||
|
AddToScheme = SchemeBuilder.AddToScheme
|
||||||
|
)
|
||||||
488
apis/actions.github.com/v1alpha1/zz_generated.deepcopy.go
Normal file
488
apis/actions.github.com/v1alpha1/zz_generated.deepcopy.go
Normal file
@@ -0,0 +1,488 @@
|
|||||||
|
//go:build !ignore_autogenerated
|
||||||
|
// +build !ignore_autogenerated
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2020 The actions-runner-controller authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Code generated by controller-gen. DO NOT EDIT.
|
||||||
|
|
||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *AutoscalingListener) DeepCopyInto(out *AutoscalingListener) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||||
|
in.Spec.DeepCopyInto(&out.Spec)
|
||||||
|
out.Status = in.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutoscalingListener.
|
||||||
|
func (in *AutoscalingListener) DeepCopy() *AutoscalingListener {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(AutoscalingListener)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *AutoscalingListener) DeepCopyObject() runtime.Object {
|
||||||
|
if c := in.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *AutoscalingListenerList) DeepCopyInto(out *AutoscalingListenerList) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||||
|
if in.Items != nil {
|
||||||
|
in, out := &in.Items, &out.Items
|
||||||
|
*out = make([]AutoscalingListener, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutoscalingListenerList.
|
||||||
|
func (in *AutoscalingListenerList) DeepCopy() *AutoscalingListenerList {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(AutoscalingListenerList)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *AutoscalingListenerList) DeepCopyObject() runtime.Object {
|
||||||
|
if c := in.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *AutoscalingListenerSpec) DeepCopyInto(out *AutoscalingListenerSpec) {
|
||||||
|
*out = *in
|
||||||
|
if in.ImagePullSecrets != nil {
|
||||||
|
in, out := &in.ImagePullSecrets, &out.ImagePullSecrets
|
||||||
|
*out = make([]v1.LocalObjectReference, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutoscalingListenerSpec.
|
||||||
|
func (in *AutoscalingListenerSpec) DeepCopy() *AutoscalingListenerSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(AutoscalingListenerSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *AutoscalingListenerStatus) DeepCopyInto(out *AutoscalingListenerStatus) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutoscalingListenerStatus.
|
||||||
|
func (in *AutoscalingListenerStatus) DeepCopy() *AutoscalingListenerStatus {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(AutoscalingListenerStatus)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *AutoscalingRunnerSet) DeepCopyInto(out *AutoscalingRunnerSet) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||||
|
in.Spec.DeepCopyInto(&out.Spec)
|
||||||
|
out.Status = in.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutoscalingRunnerSet.
|
||||||
|
func (in *AutoscalingRunnerSet) DeepCopy() *AutoscalingRunnerSet {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(AutoscalingRunnerSet)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *AutoscalingRunnerSet) DeepCopyObject() runtime.Object {
|
||||||
|
if c := in.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *AutoscalingRunnerSetList) DeepCopyInto(out *AutoscalingRunnerSetList) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||||
|
if in.Items != nil {
|
||||||
|
in, out := &in.Items, &out.Items
|
||||||
|
*out = make([]AutoscalingRunnerSet, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutoscalingRunnerSetList.
|
||||||
|
func (in *AutoscalingRunnerSetList) DeepCopy() *AutoscalingRunnerSetList {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(AutoscalingRunnerSetList)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *AutoscalingRunnerSetList) DeepCopyObject() runtime.Object {
|
||||||
|
if c := in.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *AutoscalingRunnerSetSpec) DeepCopyInto(out *AutoscalingRunnerSetSpec) {
|
||||||
|
*out = *in
|
||||||
|
if in.Proxy != nil {
|
||||||
|
in, out := &in.Proxy, &out.Proxy
|
||||||
|
*out = new(ProxyConfig)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
if in.GitHubServerTLS != nil {
|
||||||
|
in, out := &in.GitHubServerTLS, &out.GitHubServerTLS
|
||||||
|
*out = new(GitHubServerTLSConfig)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
in.Template.DeepCopyInto(&out.Template)
|
||||||
|
if in.MaxRunners != nil {
|
||||||
|
in, out := &in.MaxRunners, &out.MaxRunners
|
||||||
|
*out = new(int)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
if in.MinRunners != nil {
|
||||||
|
in, out := &in.MinRunners, &out.MinRunners
|
||||||
|
*out = new(int)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutoscalingRunnerSetSpec.
|
||||||
|
func (in *AutoscalingRunnerSetSpec) DeepCopy() *AutoscalingRunnerSetSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(AutoscalingRunnerSetSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *AutoscalingRunnerSetStatus) DeepCopyInto(out *AutoscalingRunnerSetStatus) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutoscalingRunnerSetStatus.
|
||||||
|
func (in *AutoscalingRunnerSetStatus) DeepCopy() *AutoscalingRunnerSetStatus {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(AutoscalingRunnerSetStatus)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *EphemeralRunner) DeepCopyInto(out *EphemeralRunner) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||||
|
in.Spec.DeepCopyInto(&out.Spec)
|
||||||
|
in.Status.DeepCopyInto(&out.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EphemeralRunner.
|
||||||
|
func (in *EphemeralRunner) DeepCopy() *EphemeralRunner {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(EphemeralRunner)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *EphemeralRunner) DeepCopyObject() runtime.Object {
|
||||||
|
if c := in.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *EphemeralRunnerList) DeepCopyInto(out *EphemeralRunnerList) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||||
|
if in.Items != nil {
|
||||||
|
in, out := &in.Items, &out.Items
|
||||||
|
*out = make([]EphemeralRunner, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EphemeralRunnerList.
|
||||||
|
func (in *EphemeralRunnerList) DeepCopy() *EphemeralRunnerList {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(EphemeralRunnerList)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *EphemeralRunnerList) DeepCopyObject() runtime.Object {
|
||||||
|
if c := in.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *EphemeralRunnerSet) DeepCopyInto(out *EphemeralRunnerSet) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||||
|
in.Spec.DeepCopyInto(&out.Spec)
|
||||||
|
out.Status = in.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EphemeralRunnerSet.
|
||||||
|
func (in *EphemeralRunnerSet) DeepCopy() *EphemeralRunnerSet {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(EphemeralRunnerSet)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *EphemeralRunnerSet) DeepCopyObject() runtime.Object {
|
||||||
|
if c := in.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *EphemeralRunnerSetList) DeepCopyInto(out *EphemeralRunnerSetList) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||||
|
if in.Items != nil {
|
||||||
|
in, out := &in.Items, &out.Items
|
||||||
|
*out = make([]EphemeralRunnerSet, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EphemeralRunnerSetList.
|
||||||
|
func (in *EphemeralRunnerSetList) DeepCopy() *EphemeralRunnerSetList {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(EphemeralRunnerSetList)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *EphemeralRunnerSetList) DeepCopyObject() runtime.Object {
|
||||||
|
if c := in.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *EphemeralRunnerSetSpec) DeepCopyInto(out *EphemeralRunnerSetSpec) {
|
||||||
|
*out = *in
|
||||||
|
in.EphemeralRunnerSpec.DeepCopyInto(&out.EphemeralRunnerSpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EphemeralRunnerSetSpec.
|
||||||
|
func (in *EphemeralRunnerSetSpec) DeepCopy() *EphemeralRunnerSetSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(EphemeralRunnerSetSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *EphemeralRunnerSetStatus) DeepCopyInto(out *EphemeralRunnerSetStatus) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EphemeralRunnerSetStatus.
|
||||||
|
func (in *EphemeralRunnerSetStatus) DeepCopy() *EphemeralRunnerSetStatus {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(EphemeralRunnerSetStatus)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *EphemeralRunnerSpec) DeepCopyInto(out *EphemeralRunnerSpec) {
|
||||||
|
*out = *in
|
||||||
|
if in.Proxy != nil {
|
||||||
|
in, out := &in.Proxy, &out.Proxy
|
||||||
|
*out = new(ProxyConfig)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
if in.GitHubServerTLS != nil {
|
||||||
|
in, out := &in.GitHubServerTLS, &out.GitHubServerTLS
|
||||||
|
*out = new(GitHubServerTLSConfig)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
in.PodTemplateSpec.DeepCopyInto(&out.PodTemplateSpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EphemeralRunnerSpec.
|
||||||
|
func (in *EphemeralRunnerSpec) DeepCopy() *EphemeralRunnerSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(EphemeralRunnerSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *EphemeralRunnerStatus) DeepCopyInto(out *EphemeralRunnerStatus) {
|
||||||
|
*out = *in
|
||||||
|
if in.Failures != nil {
|
||||||
|
in, out := &in.Failures, &out.Failures
|
||||||
|
*out = make(map[string]bool, len(*in))
|
||||||
|
for key, val := range *in {
|
||||||
|
(*out)[key] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EphemeralRunnerStatus.
|
||||||
|
func (in *EphemeralRunnerStatus) DeepCopy() *EphemeralRunnerStatus {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(EphemeralRunnerStatus)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *GitHubServerTLSConfig) DeepCopyInto(out *GitHubServerTLSConfig) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubServerTLSConfig.
|
||||||
|
func (in *GitHubServerTLSConfig) DeepCopy() *GitHubServerTLSConfig {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(GitHubServerTLSConfig)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *ProxyConfig) DeepCopyInto(out *ProxyConfig) {
|
||||||
|
*out = *in
|
||||||
|
if in.HTTP != nil {
|
||||||
|
in, out := &in.HTTP, &out.HTTP
|
||||||
|
*out = new(ProxyServerConfig)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
if in.HTTPS != nil {
|
||||||
|
in, out := &in.HTTPS, &out.HTTPS
|
||||||
|
*out = new(ProxyServerConfig)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyConfig.
|
||||||
|
func (in *ProxyConfig) DeepCopy() *ProxyConfig {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(ProxyConfig)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *ProxyServerConfig) DeepCopyInto(out *ProxyServerConfig) {
|
||||||
|
*out = *in
|
||||||
|
if in.NoProxy != nil {
|
||||||
|
in, out := &in.NoProxy, &out.NoProxy
|
||||||
|
*out = make([]string, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyServerConfig.
|
||||||
|
func (in *ProxyServerConfig) DeepCopy() *ProxyServerConfig {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(ProxyServerConfig)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
129
cmd/githubrunnerscalesetlistener/autoScalerKubernetesManager.go
Normal file
129
cmd/githubrunnerscalesetlistener/autoScalerKubernetesManager.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
|
||||||
|
jsonpatch "github.com/evanphx/json-patch"
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AutoScalerKubernetesManager struct {
|
||||||
|
*kubernetes.Clientset
|
||||||
|
|
||||||
|
logger logr.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKubernetesManager(logger *logr.Logger) (*AutoScalerKubernetesManager, error) {
|
||||||
|
conf, err := rest.InClusterConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
kubeClient, err := kubernetes.NewForConfig(conf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var manager = &AutoScalerKubernetesManager{
|
||||||
|
Clientset: kubeClient,
|
||||||
|
logger: logger.WithName("KubernetesManager"),
|
||||||
|
}
|
||||||
|
return manager, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *AutoScalerKubernetesManager) ScaleEphemeralRunnerSet(ctx context.Context, namespace, resourceName string, runnerCount int) error {
|
||||||
|
original := &v1alpha1.EphemeralRunnerSet{
|
||||||
|
Spec: v1alpha1.EphemeralRunnerSetSpec{
|
||||||
|
Replicas: -1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
originalJson, err := json.Marshal(original)
|
||||||
|
if err != nil {
|
||||||
|
k.logger.Error(err, "could not marshal empty ephemeral runner set")
|
||||||
|
}
|
||||||
|
|
||||||
|
patch := &v1alpha1.EphemeralRunnerSet{
|
||||||
|
Spec: v1alpha1.EphemeralRunnerSetSpec{
|
||||||
|
Replicas: runnerCount,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
patchJson, err := json.Marshal(patch)
|
||||||
|
if err != nil {
|
||||||
|
k.logger.Error(err, "could not marshal patch ephemeral runner set")
|
||||||
|
}
|
||||||
|
mergePatch, err := jsonpatch.CreateMergePatch(originalJson, patchJson)
|
||||||
|
if err != nil {
|
||||||
|
k.logger.Error(err, "could not create merge patch json for ephemeral runner set")
|
||||||
|
}
|
||||||
|
|
||||||
|
k.logger.Info("Created merge patch json for EphemeralRunnerSet update", "json", string(mergePatch))
|
||||||
|
|
||||||
|
patchedEphemeralRunnerSet := &v1alpha1.EphemeralRunnerSet{}
|
||||||
|
err = k.RESTClient().
|
||||||
|
Patch(types.MergePatchType).
|
||||||
|
Prefix("apis", "actions.github.com", "v1alpha1").
|
||||||
|
Namespace(namespace).
|
||||||
|
Resource("EphemeralRunnerSets").
|
||||||
|
Name(resourceName).
|
||||||
|
Body([]byte(mergePatch)).
|
||||||
|
Do(ctx).
|
||||||
|
Into(patchedEphemeralRunnerSet)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not patch ephemeral runner set , patch JSON: %s, error: %w", string(mergePatch), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
k.logger.Info("Ephemeral runner set scaled.", "namespace", namespace, "name", resourceName, "replicas", patchedEphemeralRunnerSet.Spec.Replicas)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *AutoScalerKubernetesManager) UpdateEphemeralRunnerWithJobInfo(ctx context.Context, namespace, resourceName, ownerName, repositoryName, jobWorkflowRef, jobDisplayName string, workflowRunId, jobRequestId int64) error {
|
||||||
|
original := &v1alpha1.EphemeralRunner{}
|
||||||
|
originalJson, err := json.Marshal(original)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not marshal empty ephemeral runner, error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
patch := &v1alpha1.EphemeralRunner{
|
||||||
|
Status: v1alpha1.EphemeralRunnerStatus{
|
||||||
|
JobRequestId: jobRequestId,
|
||||||
|
JobRepositoryName: fmt.Sprintf("%s/%s", ownerName, repositoryName),
|
||||||
|
WorkflowRunId: workflowRunId,
|
||||||
|
JobWorkflowRef: jobWorkflowRef,
|
||||||
|
JobDisplayName: jobDisplayName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
patchedJson, err := json.Marshal(patch)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not marshal patched ephemeral runner, error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mergePatch, err := jsonpatch.CreateMergePatch(originalJson, patchedJson)
|
||||||
|
if err != nil {
|
||||||
|
k.logger.Error(err, "could not create merge patch json for ephemeral runner")
|
||||||
|
}
|
||||||
|
|
||||||
|
k.logger.Info("Created merge patch json for EphemeralRunner status update", "json", string(mergePatch))
|
||||||
|
|
||||||
|
patchedStatus := &v1alpha1.EphemeralRunner{}
|
||||||
|
err = k.RESTClient().
|
||||||
|
Patch(types.MergePatchType).
|
||||||
|
Prefix("apis", "actions.github.com", "v1alpha1").
|
||||||
|
Namespace(namespace).
|
||||||
|
Resource("EphemeralRunners").
|
||||||
|
Name(resourceName).
|
||||||
|
SubResource("status").
|
||||||
|
Body(mergePatch).
|
||||||
|
Do(ctx).
|
||||||
|
Into(patchedStatus)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not patch ephemeral runner status, patch JSON: %s, error: %w", string(mergePatch), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
184
cmd/githubrunnerscalesetlistener/autoScalerMessageListener.go
Normal file
184
cmd/githubrunnerscalesetlistener/autoScalerMessageListener.go
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sessionCreationMaxRetryCount = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
type devContextKey bool
|
||||||
|
|
||||||
|
var testIgnoreSleep devContextKey = true
|
||||||
|
|
||||||
|
type AutoScalerClient struct {
|
||||||
|
client actions.SessionService
|
||||||
|
logger logr.Logger
|
||||||
|
|
||||||
|
lastMessageId int64
|
||||||
|
initialMessage *actions.RunnerScaleSetMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAutoScalerClient(
|
||||||
|
ctx context.Context,
|
||||||
|
client actions.ActionsService,
|
||||||
|
logger *logr.Logger,
|
||||||
|
runnerScaleSetId int,
|
||||||
|
options ...func(*AutoScalerClient),
|
||||||
|
) (*AutoScalerClient, error) {
|
||||||
|
listener := AutoScalerClient{
|
||||||
|
logger: logger.WithName("auto_scaler"),
|
||||||
|
}
|
||||||
|
|
||||||
|
session, initialMessage, err := createSession(ctx, &listener.logger, client, runnerScaleSetId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fail to create session. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
listener.lastMessageId = 0
|
||||||
|
listener.initialMessage = initialMessage
|
||||||
|
listener.client = newSessionClient(client, logger, session)
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
option(&listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &listener, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSession(ctx context.Context, logger *logr.Logger, client actions.ActionsService, runnerScaleSetId int) (*actions.RunnerScaleSetSession, *actions.RunnerScaleSetMessage, error) {
|
||||||
|
hostName, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
hostName = uuid.New().String()
|
||||||
|
logger.Info("could not get hostname, fail back to a random string.", "fallback", hostName)
|
||||||
|
}
|
||||||
|
|
||||||
|
var runnerScaleSetSession *actions.RunnerScaleSetSession
|
||||||
|
var retryCount int
|
||||||
|
for {
|
||||||
|
runnerScaleSetSession, err = client.CreateMessageSession(ctx, runnerScaleSetId, hostName)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
clientSideError := &actions.HttpClientSideError{}
|
||||||
|
if errors.As(err, &clientSideError) && clientSideError.Code != http.StatusConflict {
|
||||||
|
logger.Info("unable to create message session. The error indicates something is wrong on the client side, won't make any retry.")
|
||||||
|
return nil, nil, fmt.Errorf("create message session http request failed. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
retryCount++
|
||||||
|
if retryCount >= sessionCreationMaxRetryCount {
|
||||||
|
return nil, nil, fmt.Errorf("create message session failed since it exceed %d retry limit. %w", sessionCreationMaxRetryCount, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("unable to create message session. Will try again in 30 seconds", "error", err.Error())
|
||||||
|
if ok := ctx.Value(testIgnoreSleep); ok == nil {
|
||||||
|
time.Sleep(getRandomDuration(30, 45))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statistics, _ := json.Marshal(runnerScaleSetSession.Statistics)
|
||||||
|
logger.Info("current runner scale set statistics.", "statistics", string(statistics))
|
||||||
|
|
||||||
|
if runnerScaleSetSession.Statistics.TotalAvailableJobs > 0 || runnerScaleSetSession.Statistics.TotalAssignedJobs > 0 {
|
||||||
|
acquirableJobs, err := client.GetAcquirableJobs(ctx, runnerScaleSetId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("get acquirable jobs failed. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
acquirableJobsJson, err := json.Marshal(acquirableJobs.Jobs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("marshal acquirable jobs failed. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
initialMessage := &actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 0,
|
||||||
|
MessageType: "RunnerScaleSetJobMessages",
|
||||||
|
Statistics: runnerScaleSetSession.Statistics,
|
||||||
|
Body: string(acquirableJobsJson),
|
||||||
|
}
|
||||||
|
|
||||||
|
return runnerScaleSetSession, initialMessage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return runnerScaleSetSession, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AutoScalerClient) Close() error {
|
||||||
|
m.logger.Info("closing.")
|
||||||
|
return m.client.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AutoScalerClient) GetRunnerScaleSetMessage(ctx context.Context, handler func(msg *actions.RunnerScaleSetMessage) error) error {
|
||||||
|
if m.initialMessage != nil {
|
||||||
|
err := handler(m.initialMessage)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fail to process initial message. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.initialMessage = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
message, err := m.client.GetMessage(ctx, m.lastMessageId)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get message failed from refreshing client. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if message == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler(message)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("handle message failed. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.lastMessageId = message.MessageId
|
||||||
|
|
||||||
|
return m.deleteMessage(ctx, message.MessageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AutoScalerClient) deleteMessage(ctx context.Context, messageId int64) error {
|
||||||
|
err := m.client.DeleteMessage(ctx, messageId)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("delete message failed from refreshing client. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.logger.Info("deleted message.", "messageId", messageId)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AutoScalerClient) AcquireJobsForRunnerScaleSet(ctx context.Context, requestIds []int64) error {
|
||||||
|
m.logger.Info("acquiring jobs.", "request count", len(requestIds), "requestIds", fmt.Sprint(requestIds))
|
||||||
|
if len(requestIds) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ids, err := m.client.AcquireJobs(ctx, requestIds)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("acquire jobs failed from refreshing client. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.logger.Info("acquired jobs.", "requested", len(requestIds), "acquired", len(ids))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRandomDuration(minSeconds, maxSeconds int) time.Duration {
|
||||||
|
return time.Duration(rand.Intn(maxSeconds-minSeconds)+minSeconds) * time.Second
|
||||||
|
}
|
||||||
@@ -0,0 +1,701 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
"github.com/actions/actions-runner-controller/logging"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateSession(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||||
|
|
||||||
|
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1)
|
||||||
|
|
||||||
|
require.NoError(t, err, "Error creating autoscaler client")
|
||||||
|
assert.Equal(t, session, session, "Session is not correct")
|
||||||
|
assert.Nil(t, asClient.initialMessage, "Initial message should be nil")
|
||||||
|
assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be 0")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSession_CreateInitMessage(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{
|
||||||
|
TotalAvailableJobs: 1,
|
||||||
|
TotalAssignedJobs: 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||||
|
mockActionsClient.On("GetAcquirableJobs", ctx, 1).Return(&actions.AcquirableJobList{
|
||||||
|
Count: 1,
|
||||||
|
Jobs: []actions.AcquirableJob{
|
||||||
|
{
|
||||||
|
RunnerRequestId: 1,
|
||||||
|
OwnerName: "owner",
|
||||||
|
RepositoryName: "repo",
|
||||||
|
AcquireJobUrl: "https://github.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1)
|
||||||
|
|
||||||
|
require.NoError(t, err, "Error creating autoscaler client")
|
||||||
|
assert.Equal(t, session, session, "Session is not correct")
|
||||||
|
assert.NotNil(t, asClient.initialMessage, "Initial message should not be nil")
|
||||||
|
assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be 0")
|
||||||
|
assert.Equal(t, int64(0), asClient.initialMessage.MessageId, "Initial message id should be 0")
|
||||||
|
assert.Equal(t, "RunnerScaleSetJobMessages", asClient.initialMessage.MessageType, "Initial message type should be RunnerScaleSetJobMessages")
|
||||||
|
assert.Equal(t, 5, asClient.initialMessage.Statistics.TotalAssignedJobs, "Initial message total assigned jobs should be 5")
|
||||||
|
assert.Equal(t, 1, asClient.initialMessage.Statistics.TotalAvailableJobs, "Initial message total available jobs should be 1")
|
||||||
|
assert.Equal(t, "[{\"acquireJobUrl\":\"https://github.com\",\"messageType\":\"\",\"runnerRequestId\":1,\"repositoryName\":\"repo\",\"ownerName\":\"owner\",\"jobWorkflowRef\":\"\",\"eventName\":\"\",\"requestLabels\":null}]", asClient.initialMessage.Body, "Initial message body is not correct")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSession_CreateInitMessageWithOnlyAssignedJobs(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{
|
||||||
|
TotalAssignedJobs: 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||||
|
mockActionsClient.On("GetAcquirableJobs", ctx, 1).Return(&actions.AcquirableJobList{
|
||||||
|
Count: 0,
|
||||||
|
Jobs: []actions.AcquirableJob{},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1)
|
||||||
|
|
||||||
|
require.NoError(t, err, "Error creating autoscaler client")
|
||||||
|
assert.Equal(t, session, session, "Session is not correct")
|
||||||
|
assert.NotNil(t, asClient.initialMessage, "Initial message should not be nil")
|
||||||
|
assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be 0")
|
||||||
|
assert.Equal(t, int64(0), asClient.initialMessage.MessageId, "Initial message id should be 0")
|
||||||
|
assert.Equal(t, "RunnerScaleSetJobMessages", asClient.initialMessage.MessageType, "Initial message type should be RunnerScaleSetJobMessages")
|
||||||
|
assert.Equal(t, 5, asClient.initialMessage.Statistics.TotalAssignedJobs, "Initial message total assigned jobs should be 5")
|
||||||
|
assert.Equal(t, 0, asClient.initialMessage.Statistics.TotalAvailableJobs, "Initial message total available jobs should be 0")
|
||||||
|
assert.Equal(t, "[]", asClient.initialMessage.Body, "Initial message body is not correct")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSession_CreateInitMessageFailed(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{
|
||||||
|
TotalAvailableJobs: 1,
|
||||||
|
TotalAssignedJobs: 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||||
|
mockActionsClient.On("GetAcquirableJobs", ctx, 1).Return(nil, fmt.Errorf("error"))
|
||||||
|
|
||||||
|
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1)
|
||||||
|
|
||||||
|
assert.ErrorContains(t, err, "get acquirable jobs failed. error", "Unexpected error")
|
||||||
|
assert.Nil(t, asClient, "Client should be nil")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSession_RetrySessionConflict(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.WithValue(context.Background(), testIgnoreSleep, true)
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(nil, &actions.HttpClientSideError{
|
||||||
|
Code: 409,
|
||||||
|
}).Once()
|
||||||
|
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil).Once()
|
||||||
|
|
||||||
|
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1)
|
||||||
|
|
||||||
|
require.NoError(t, err, "Error creating autoscaler client")
|
||||||
|
assert.Equal(t, session, session, "Session is not correct")
|
||||||
|
assert.Nil(t, asClient.initialMessage, "Initial message should be nil")
|
||||||
|
assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be 0")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSession_RetrySessionConflict_RunOutOfRetry(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.WithValue(context.Background(), testIgnoreSleep, true)
|
||||||
|
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(nil, &actions.HttpClientSideError{
|
||||||
|
Code: 409,
|
||||||
|
})
|
||||||
|
|
||||||
|
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1)
|
||||||
|
|
||||||
|
assert.Error(t, err, "Error should be returned")
|
||||||
|
assert.Nil(t, asClient, "AutoScaler should be nil")
|
||||||
|
assert.True(t, mockActionsClient.AssertNumberOfCalls(t, "CreateMessageSession", sessionCreationMaxRetryCount), "CreateMessageSession should be called 10 times")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSession_NotRetryOnGeneralException(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.WithValue(context.Background(), testIgnoreSleep, true)
|
||||||
|
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(nil, &actions.HttpClientSideError{
|
||||||
|
Code: 403,
|
||||||
|
})
|
||||||
|
|
||||||
|
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1)
|
||||||
|
|
||||||
|
assert.Error(t, err, "Error should be returned")
|
||||||
|
assert.Nil(t, asClient, "AutoScaler should be nil")
|
||||||
|
assert.True(t, mockActionsClient.AssertNumberOfCalls(t, "CreateMessageSession", 1), "CreateMessageSession should be called 1 time and not retry on generic error")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteSession(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
mockSessionClient := &actions.MockSessionService{}
|
||||||
|
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||||
|
mockSessionClient.On("Close").Return(nil)
|
||||||
|
|
||||||
|
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) {
|
||||||
|
asc.client = mockSessionClient
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "Error creating autoscaler client")
|
||||||
|
|
||||||
|
err = asClient.Close()
|
||||||
|
assert.NoError(t, err, "Error deleting session")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteSession_Failed(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
mockSessionClient := &actions.MockSessionService{}
|
||||||
|
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||||
|
mockSessionClient.On("Close").Return(fmt.Errorf("error"))
|
||||||
|
|
||||||
|
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) {
|
||||||
|
asc.client = mockSessionClient
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "Error creating autoscaler client")
|
||||||
|
|
||||||
|
err = asClient.Close()
|
||||||
|
assert.Error(t, err, "Error should be returned")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRunnerScaleSetMessage(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
mockSessionClient := &actions.MockSessionService{}
|
||||||
|
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||||
|
mockSessionClient.On("GetMessage", ctx, int64(0)).Return(&actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "test",
|
||||||
|
Body: "test",
|
||||||
|
}, nil)
|
||||||
|
mockSessionClient.On("DeleteMessage", ctx, int64(1)).Return(nil)
|
||||||
|
|
||||||
|
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) {
|
||||||
|
asc.client = mockSessionClient
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "Error creating autoscaler client")
|
||||||
|
|
||||||
|
err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error {
|
||||||
|
logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err, "Error getting message")
|
||||||
|
assert.Equal(t, int64(1), asClient.lastMessageId, "Last message id should be updated")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRunnerScaleSetMessage_HandleFailed(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
mockSessionClient := &actions.MockSessionService{}
|
||||||
|
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||||
|
mockSessionClient.On("GetMessage", ctx, int64(0)).Return(&actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "test",
|
||||||
|
Body: "test",
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) {
|
||||||
|
asc.client = mockSessionClient
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "Error creating autoscaler client")
|
||||||
|
|
||||||
|
err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error {
|
||||||
|
logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body)
|
||||||
|
return fmt.Errorf("error")
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.ErrorContains(t, err, "handle message failed. error", "Error getting message")
|
||||||
|
assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be updated")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRunnerScaleSetMessage_HandleInitialMessage(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{
|
||||||
|
TotalAvailableJobs: 1,
|
||||||
|
TotalAssignedJobs: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||||
|
mockActionsClient.On("GetAcquirableJobs", ctx, 1).Return(&actions.AcquirableJobList{
|
||||||
|
Count: 1,
|
||||||
|
Jobs: []actions.AcquirableJob{
|
||||||
|
{
|
||||||
|
RunnerRequestId: 1,
|
||||||
|
OwnerName: "owner",
|
||||||
|
RepositoryName: "repo",
|
||||||
|
AcquireJobUrl: "https://github.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1)
|
||||||
|
require.NoError(t, err, "Error creating autoscaler client")
|
||||||
|
require.NotNil(t, asClient.initialMessage, "Initial message should be set")
|
||||||
|
|
||||||
|
err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error {
|
||||||
|
logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err, "Error getting message")
|
||||||
|
assert.Nil(t, asClient.initialMessage, "Initial message should be nil")
|
||||||
|
assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be updated")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRunnerScaleSetMessage_HandleInitialMessageFailed(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{
|
||||||
|
TotalAvailableJobs: 1,
|
||||||
|
TotalAssignedJobs: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||||
|
mockActionsClient.On("GetAcquirableJobs", ctx, 1).Return(&actions.AcquirableJobList{
|
||||||
|
Count: 1,
|
||||||
|
Jobs: []actions.AcquirableJob{
|
||||||
|
{
|
||||||
|
RunnerRequestId: 1,
|
||||||
|
OwnerName: "owner",
|
||||||
|
RepositoryName: "repo",
|
||||||
|
AcquireJobUrl: "https://github.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1)
|
||||||
|
require.NoError(t, err, "Error creating autoscaler client")
|
||||||
|
require.NotNil(t, asClient.initialMessage, "Initial message should be set")
|
||||||
|
|
||||||
|
err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error {
|
||||||
|
logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body)
|
||||||
|
return fmt.Errorf("error")
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.ErrorContains(t, err, "fail to process initial message. error", "Error getting message")
|
||||||
|
assert.NotNil(t, asClient.initialMessage, "Initial message should be nil")
|
||||||
|
assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be updated")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRunnerScaleSetMessage_RetryUntilGetMessage(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
mockSessionClient := &actions.MockSessionService{}
|
||||||
|
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||||
|
mockSessionClient.On("GetMessage", ctx, int64(0)).Return(nil, nil).Times(3)
|
||||||
|
mockSessionClient.On("GetMessage", ctx, int64(0)).Return(&actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "test",
|
||||||
|
Body: "test",
|
||||||
|
}, nil).Once()
|
||||||
|
mockSessionClient.On("DeleteMessage", ctx, int64(1)).Return(nil)
|
||||||
|
|
||||||
|
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) {
|
||||||
|
asc.client = mockSessionClient
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "Error creating autoscaler client")
|
||||||
|
|
||||||
|
err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error {
|
||||||
|
logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err, "Error getting message")
|
||||||
|
assert.Equal(t, int64(1), asClient.lastMessageId, "Last message id should be updated")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRunnerScaleSetMessage_ErrorOnGetMessage(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
mockSessionClient := &actions.MockSessionService{}
|
||||||
|
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||||
|
mockSessionClient.On("GetMessage", ctx, int64(0)).Return(nil, fmt.Errorf("error"))
|
||||||
|
|
||||||
|
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) {
|
||||||
|
asc.client = mockSessionClient
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "Error creating autoscaler client")
|
||||||
|
|
||||||
|
err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error {
|
||||||
|
return fmt.Errorf("Should not be called")
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.ErrorContains(t, err, "get message failed from refreshing client. error", "Error should be returned")
|
||||||
|
assert.Equal(t, int64(0), asClient.lastMessageId, "Last message id should be updated")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteRunnerScaleSetMessage_Error(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
mockSessionClient := &actions.MockSessionService{}
|
||||||
|
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||||
|
mockSessionClient.On("GetMessage", ctx, int64(0)).Return(&actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "test",
|
||||||
|
Body: "test",
|
||||||
|
}, nil)
|
||||||
|
mockSessionClient.On("DeleteMessage", ctx, int64(1)).Return(fmt.Errorf("error"))
|
||||||
|
|
||||||
|
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) {
|
||||||
|
asc.client = mockSessionClient
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "Error creating autoscaler client")
|
||||||
|
|
||||||
|
err = asClient.GetRunnerScaleSetMessage(ctx, func(msg *actions.RunnerScaleSetMessage) error {
|
||||||
|
logger.Info("Message received", "messageId", msg.MessageId, "messageType", msg.MessageType, "body", msg.Body)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.ErrorContains(t, err, "delete message failed from refreshing client. error", "Error getting message")
|
||||||
|
assert.Equal(t, int64(1), asClient.lastMessageId, "Last message id should be updated")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAcquireJobsForRunnerScaleSet(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
mockSessionClient := &actions.MockSessionService{}
|
||||||
|
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||||
|
mockSessionClient.On("AcquireJobs", ctx, mock.MatchedBy(func(ids []int64) bool { return ids[0] == 1 && ids[1] == 2 && ids[2] == 3 })).Return([]int64{1, 2, 3}, nil)
|
||||||
|
|
||||||
|
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) {
|
||||||
|
asc.client = mockSessionClient
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "Error creating autoscaler client")
|
||||||
|
|
||||||
|
err = asClient.AcquireJobsForRunnerScaleSet(ctx, []int64{1, 2, 3})
|
||||||
|
assert.NoError(t, err, "Error acquiring jobs")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAcquireJobsForRunnerScaleSet_SkipEmptyList(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
mockSessionClient := &actions.MockSessionService{}
|
||||||
|
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||||
|
|
||||||
|
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) {
|
||||||
|
asc.client = mockSessionClient
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "Error creating autoscaler client")
|
||||||
|
|
||||||
|
err = asClient.AcquireJobsForRunnerScaleSet(ctx, []int64{})
|
||||||
|
assert.NoError(t, err, "Error acquiring jobs")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAcquireJobsForRunnerScaleSet_Failed(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
mockSessionClient := &actions.MockSessionService{}
|
||||||
|
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("CreateMessageSession", ctx, 1, mock.Anything).Return(session, nil)
|
||||||
|
mockSessionClient.On("AcquireJobs", ctx, mock.Anything).Return(nil, fmt.Errorf("error"))
|
||||||
|
|
||||||
|
asClient, err := NewAutoScalerClient(ctx, mockActionsClient, &logger, 1, func(asc *AutoScalerClient) {
|
||||||
|
asc.client = mockSessionClient
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "Error creating autoscaler client")
|
||||||
|
|
||||||
|
err = asClient.AcquireJobsForRunnerScaleSet(ctx, []int64{1, 2, 3})
|
||||||
|
assert.ErrorContains(t, err, "acquire jobs failed from refreshing client. error", "Expect error acquiring jobs")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockSessionClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
185
cmd/githubrunnerscalesetlistener/autoScalerService.go
Normal file
185
cmd/githubrunnerscalesetlistener/autoScalerService.go
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ScaleSettings struct {
|
||||||
|
Namespace string
|
||||||
|
ResourceName string
|
||||||
|
MinRunners int
|
||||||
|
MaxRunners int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
ctx context.Context
|
||||||
|
logger logr.Logger
|
||||||
|
rsClient RunnerScaleSetClient
|
||||||
|
kubeManager KubernetesManager
|
||||||
|
settings *ScaleSettings
|
||||||
|
currentRunnerCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(
|
||||||
|
ctx context.Context,
|
||||||
|
rsClient RunnerScaleSetClient,
|
||||||
|
manager KubernetesManager,
|
||||||
|
settings *ScaleSettings,
|
||||||
|
options ...func(*Service),
|
||||||
|
) *Service {
|
||||||
|
s := &Service{
|
||||||
|
ctx: ctx,
|
||||||
|
rsClient: rsClient,
|
||||||
|
kubeManager: manager,
|
||||||
|
settings: settings,
|
||||||
|
currentRunnerCount: 0,
|
||||||
|
logger: logr.FromContextOrDiscard(ctx),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
option(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Start() error {
|
||||||
|
if s.settings.MinRunners > 0 {
|
||||||
|
s.logger.Info("scale to match minimal runners.")
|
||||||
|
err := s.scaleForAssignedJobCount(0)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not scale to match minimal runners. %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
s.logger.Info("waiting for message...")
|
||||||
|
select {
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
s.logger.Info("service is stopped.")
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
err := s.rsClient.GetRunnerScaleSetMessage(s.ctx, s.processMessage)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not get and process message. %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) processMessage(message *actions.RunnerScaleSetMessage) error {
|
||||||
|
s.logger.Info("process message.", "messageId", message.MessageId, "messageType", message.MessageType)
|
||||||
|
if message.Statistics == nil {
|
||||||
|
return fmt.Errorf("can't process message with empty statistics")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("current runner scale set statistics.",
|
||||||
|
"available jobs", message.Statistics.TotalAvailableJobs,
|
||||||
|
"acquired jobs", message.Statistics.TotalAcquiredJobs,
|
||||||
|
"assigned jobs", message.Statistics.TotalAssignedJobs,
|
||||||
|
"running jobs", message.Statistics.TotalRunningJobs,
|
||||||
|
"registered runners", message.Statistics.TotalRegisteredRunners,
|
||||||
|
"busy runners", message.Statistics.TotalBusyRunners,
|
||||||
|
"idle runners", message.Statistics.TotalIdleRunners)
|
||||||
|
|
||||||
|
if message.MessageType != "RunnerScaleSetJobMessages" {
|
||||||
|
s.logger.Info("skip message with unknown message type.", "messageType", message.MessageType)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var batchedMessages []json.RawMessage
|
||||||
|
if err := json.NewDecoder(strings.NewReader(message.Body)).Decode(&batchedMessages); err != nil {
|
||||||
|
return fmt.Errorf("could not decode job messages. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("process batched runner scale set job messages.", "messageId", message.MessageId, "batchSize", len(batchedMessages))
|
||||||
|
|
||||||
|
var availableJobs []int64
|
||||||
|
for _, message := range batchedMessages {
|
||||||
|
var messageType actions.JobMessageType
|
||||||
|
if err := json.Unmarshal(message, &messageType); err != nil {
|
||||||
|
return fmt.Errorf("could not decode job message type. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch messageType.MessageType {
|
||||||
|
case "JobAvailable":
|
||||||
|
var jobAvailable actions.JobAvailable
|
||||||
|
if err := json.Unmarshal(message, &jobAvailable); err != nil {
|
||||||
|
return fmt.Errorf("could not decode job available message. %w", err)
|
||||||
|
}
|
||||||
|
s.logger.Info("job available message received.", "RequestId", jobAvailable.RunnerRequestId)
|
||||||
|
availableJobs = append(availableJobs, jobAvailable.RunnerRequestId)
|
||||||
|
case "JobAssigned":
|
||||||
|
var jobAssigned actions.JobAssigned
|
||||||
|
if err := json.Unmarshal(message, &jobAssigned); err != nil {
|
||||||
|
return fmt.Errorf("could not decode job assigned message. %w", err)
|
||||||
|
}
|
||||||
|
s.logger.Info("job assigned message received.", "RequestId", jobAssigned.RunnerRequestId)
|
||||||
|
case "JobStarted":
|
||||||
|
var jobStarted actions.JobStarted
|
||||||
|
if err := json.Unmarshal(message, &jobStarted); err != nil {
|
||||||
|
return fmt.Errorf("could not decode job started message. %w", err)
|
||||||
|
}
|
||||||
|
s.logger.Info("job started message received.", "RequestId", jobStarted.RunnerRequestId, "RunnerId", jobStarted.RunnerId)
|
||||||
|
s.updateJobInfoForRunner(jobStarted)
|
||||||
|
case "JobCompleted":
|
||||||
|
var jobCompleted actions.JobCompleted
|
||||||
|
if err := json.Unmarshal(message, &jobCompleted); err != nil {
|
||||||
|
return fmt.Errorf("could not decode job completed message. %w", err)
|
||||||
|
}
|
||||||
|
s.logger.Info("job completed message received.", "RequestId", jobCompleted.RunnerRequestId, "Result", jobCompleted.Result, "RunnerId", jobCompleted.RunnerId, "RunnerName", jobCompleted.RunnerName)
|
||||||
|
default:
|
||||||
|
s.logger.Info("unknown job message type.", "messageType", messageType.MessageType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.rsClient.AcquireJobsForRunnerScaleSet(s.ctx, availableJobs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not acquire jobs. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.scaleForAssignedJobCount(message.Statistics.TotalAssignedJobs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) scaleForAssignedJobCount(count int) error {
|
||||||
|
targetRunnerCount := int(math.Max(math.Min(float64(s.settings.MaxRunners), float64(count)), float64(s.settings.MinRunners)))
|
||||||
|
if targetRunnerCount != s.currentRunnerCount {
|
||||||
|
s.logger.Info("try scale runner request up/down base on assigned job count",
|
||||||
|
"assigned job", count,
|
||||||
|
"decision", targetRunnerCount,
|
||||||
|
"min", s.settings.MinRunners,
|
||||||
|
"max", s.settings.MaxRunners,
|
||||||
|
"currentRunnerCount", s.currentRunnerCount)
|
||||||
|
err := s.kubeManager.ScaleEphemeralRunnerSet(s.ctx, s.settings.Namespace, s.settings.ResourceName, targetRunnerCount)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not scale ephemeral runner set (%s/%s). %w", s.settings.Namespace, s.settings.ResourceName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.currentRunnerCount = targetRunnerCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateJobInfoForRunner updates the ephemeral runner with the job info and this is best effort since the info is only for better telemetry
|
||||||
|
func (s *Service) updateJobInfoForRunner(jobInfo actions.JobStarted) {
|
||||||
|
s.logger.Info("update job info for runner",
|
||||||
|
"runnerName", jobInfo.RunnerName,
|
||||||
|
"ownerName", jobInfo.OwnerName,
|
||||||
|
"repoName", jobInfo.RepositoryName,
|
||||||
|
"workflowRef", jobInfo.JobWorkflowRef,
|
||||||
|
"workflowRunId", jobInfo.WorkflowRunId,
|
||||||
|
"jobDisplayName", jobInfo.JobDisplayName,
|
||||||
|
"requestId", jobInfo.RunnerRequestId)
|
||||||
|
err := s.kubeManager.UpdateEphemeralRunnerWithJobInfo(s.ctx, s.settings.Namespace, jobInfo.RunnerName, jobInfo.OwnerName, jobInfo.RepositoryName, jobInfo.JobWorkflowRef, jobInfo.JobDisplayName, jobInfo.WorkflowRunId, jobInfo.RunnerRequestId)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error(err, "could not update ephemeral runner with job info", "runnerName", jobInfo.RunnerName, "requestId", jobInfo.RunnerRequestId)
|
||||||
|
}
|
||||||
|
}
|
||||||
631
cmd/githubrunnerscalesetlistener/autoScalerService_test.go
Normal file
631
cmd/githubrunnerscalesetlistener/autoScalerService_test.go
Normal file
@@ -0,0 +1,631 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
"github.com/actions/actions-runner-controller/logging"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewService(t *testing.T) {
|
||||||
|
mockRsClient := &MockRunnerScaleSetClient{}
|
||||||
|
mockKubeManager := &MockKubernetesManager{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
service := NewService(
|
||||||
|
ctx,
|
||||||
|
mockRsClient,
|
||||||
|
mockKubeManager,
|
||||||
|
&ScaleSettings{
|
||||||
|
Namespace: "namespace",
|
||||||
|
ResourceName: "resource",
|
||||||
|
MinRunners: 0,
|
||||||
|
MaxRunners: 5,
|
||||||
|
},
|
||||||
|
func(s *Service) {
|
||||||
|
s.logger = logger
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Equal(t, logger, service.logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStart(t *testing.T) {
|
||||||
|
mockRsClient := &MockRunnerScaleSetClient{}
|
||||||
|
mockKubeManager := &MockKubernetesManager{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
service := NewService(
|
||||||
|
ctx,
|
||||||
|
mockRsClient,
|
||||||
|
mockKubeManager,
|
||||||
|
&ScaleSettings{
|
||||||
|
Namespace: "namespace",
|
||||||
|
ResourceName: "resource",
|
||||||
|
MinRunners: 0,
|
||||||
|
MaxRunners: 5,
|
||||||
|
},
|
||||||
|
func(s *Service) {
|
||||||
|
s.logger = logger
|
||||||
|
},
|
||||||
|
)
|
||||||
|
mockRsClient.On("GetRunnerScaleSetMessage", service.ctx, mock.Anything).Run(func(args mock.Arguments) { cancel() }).Return(nil).Once()
|
||||||
|
|
||||||
|
err := service.Start()
|
||||||
|
|
||||||
|
assert.NoError(t, err, "Unexpected error")
|
||||||
|
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStart_ScaleToMinRunners(t *testing.T) {
|
||||||
|
mockRsClient := &MockRunnerScaleSetClient{}
|
||||||
|
mockKubeManager := &MockKubernetesManager{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
service := NewService(
|
||||||
|
ctx,
|
||||||
|
mockRsClient,
|
||||||
|
mockKubeManager,
|
||||||
|
&ScaleSettings{
|
||||||
|
Namespace: "namespace",
|
||||||
|
ResourceName: "resource",
|
||||||
|
MinRunners: 5,
|
||||||
|
MaxRunners: 5,
|
||||||
|
},
|
||||||
|
func(s *Service) {
|
||||||
|
s.logger = logger
|
||||||
|
},
|
||||||
|
)
|
||||||
|
mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 5).Run(func(args mock.Arguments) { cancel() }).Return(nil).Once()
|
||||||
|
|
||||||
|
err := service.Start()
|
||||||
|
|
||||||
|
assert.NoError(t, err, "Unexpected error")
|
||||||
|
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStart_ScaleToMinRunnersFailed(t *testing.T) {
|
||||||
|
mockRsClient := &MockRunnerScaleSetClient{}
|
||||||
|
mockKubeManager := &MockKubernetesManager{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
service := NewService(
|
||||||
|
ctx,
|
||||||
|
mockRsClient,
|
||||||
|
mockKubeManager,
|
||||||
|
&ScaleSettings{
|
||||||
|
Namespace: "namespace",
|
||||||
|
ResourceName: "resource",
|
||||||
|
MinRunners: 5,
|
||||||
|
MaxRunners: 5,
|
||||||
|
},
|
||||||
|
func(s *Service) {
|
||||||
|
s.logger = logger
|
||||||
|
},
|
||||||
|
)
|
||||||
|
mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 5).Return(fmt.Errorf("error")).Once()
|
||||||
|
|
||||||
|
err := service.Start()
|
||||||
|
|
||||||
|
assert.ErrorContains(t, err, "could not scale to match minimal runners", "Unexpected error")
|
||||||
|
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStart_GetMultipleMessages(t *testing.T) {
|
||||||
|
mockRsClient := &MockRunnerScaleSetClient{}
|
||||||
|
mockKubeManager := &MockKubernetesManager{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
service := NewService(
|
||||||
|
ctx,
|
||||||
|
mockRsClient,
|
||||||
|
mockKubeManager,
|
||||||
|
&ScaleSettings{
|
||||||
|
Namespace: "namespace",
|
||||||
|
ResourceName: "resource",
|
||||||
|
MinRunners: 0,
|
||||||
|
MaxRunners: 5,
|
||||||
|
},
|
||||||
|
func(s *Service) {
|
||||||
|
s.logger = logger
|
||||||
|
},
|
||||||
|
)
|
||||||
|
mockRsClient.On("GetRunnerScaleSetMessage", service.ctx, mock.Anything).Return(nil).Times(5)
|
||||||
|
mockRsClient.On("GetRunnerScaleSetMessage", service.ctx, mock.Anything).Run(func(args mock.Arguments) { cancel() }).Return(nil).Once()
|
||||||
|
|
||||||
|
err := service.Start()
|
||||||
|
|
||||||
|
assert.NoError(t, err, "Unexpected error")
|
||||||
|
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStart_ErrorOnMessage(t *testing.T) {
|
||||||
|
mockRsClient := &MockRunnerScaleSetClient{}
|
||||||
|
mockKubeManager := &MockKubernetesManager{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
service := NewService(
|
||||||
|
ctx,
|
||||||
|
mockRsClient,
|
||||||
|
mockKubeManager,
|
||||||
|
&ScaleSettings{
|
||||||
|
Namespace: "namespace",
|
||||||
|
ResourceName: "resource",
|
||||||
|
MinRunners: 0,
|
||||||
|
MaxRunners: 5,
|
||||||
|
},
|
||||||
|
func(s *Service) {
|
||||||
|
s.logger = logger
|
||||||
|
},
|
||||||
|
)
|
||||||
|
mockRsClient.On("GetRunnerScaleSetMessage", service.ctx, mock.Anything).Return(nil).Times(2)
|
||||||
|
mockRsClient.On("GetRunnerScaleSetMessage", service.ctx, mock.Anything).Return(fmt.Errorf("error")).Once()
|
||||||
|
|
||||||
|
err := service.Start()
|
||||||
|
|
||||||
|
assert.ErrorContains(t, err, "could not get and process message. error", "Unexpected error")
|
||||||
|
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessMessage_NoStatistic(t *testing.T) {
|
||||||
|
mockRsClient := &MockRunnerScaleSetClient{}
|
||||||
|
mockKubeManager := &MockKubernetesManager{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
service := NewService(
|
||||||
|
ctx,
|
||||||
|
mockRsClient,
|
||||||
|
mockKubeManager,
|
||||||
|
&ScaleSettings{
|
||||||
|
Namespace: "namespace",
|
||||||
|
ResourceName: "resource",
|
||||||
|
MinRunners: 0,
|
||||||
|
MaxRunners: 5,
|
||||||
|
},
|
||||||
|
func(s *Service) {
|
||||||
|
s.logger = logger
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
err := service.processMessage(&actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "test",
|
||||||
|
Body: "test",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.ErrorContains(t, err, "can't process message with empty statistics", "Unexpected error")
|
||||||
|
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessMessage_IgnoreUnknownMessageType(t *testing.T) {
|
||||||
|
mockRsClient := &MockRunnerScaleSetClient{}
|
||||||
|
mockKubeManager := &MockKubernetesManager{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
service := NewService(
|
||||||
|
ctx,
|
||||||
|
mockRsClient,
|
||||||
|
mockKubeManager,
|
||||||
|
&ScaleSettings{
|
||||||
|
Namespace: "namespace",
|
||||||
|
ResourceName: "resource",
|
||||||
|
MinRunners: 0,
|
||||||
|
MaxRunners: 5,
|
||||||
|
},
|
||||||
|
func(s *Service) {
|
||||||
|
s.logger = logger
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
err := service.processMessage(&actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "unknown",
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{
|
||||||
|
TotalAvailableJobs: 1,
|
||||||
|
},
|
||||||
|
Body: "[]",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err, "Unexpected error")
|
||||||
|
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessMessage_InvalidBatchMessageJson(t *testing.T) {
|
||||||
|
mockRsClient := &MockRunnerScaleSetClient{}
|
||||||
|
mockKubeManager := &MockKubernetesManager{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
service := NewService(
|
||||||
|
ctx,
|
||||||
|
mockRsClient,
|
||||||
|
mockKubeManager,
|
||||||
|
&ScaleSettings{
|
||||||
|
Namespace: "namespace",
|
||||||
|
ResourceName: "resource",
|
||||||
|
MinRunners: 0,
|
||||||
|
MaxRunners: 5,
|
||||||
|
},
|
||||||
|
func(s *Service) {
|
||||||
|
s.logger = logger
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
err := service.processMessage(&actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "RunnerScaleSetJobMessages",
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{
|
||||||
|
TotalAvailableJobs: 1,
|
||||||
|
},
|
||||||
|
Body: "invalid json",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.ErrorContains(t, err, "could not decode job messages", "Unexpected error")
|
||||||
|
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessMessage_InvalidJobMessageJson(t *testing.T) {
|
||||||
|
mockRsClient := &MockRunnerScaleSetClient{}
|
||||||
|
mockKubeManager := &MockKubernetesManager{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
service := NewService(
|
||||||
|
ctx,
|
||||||
|
mockRsClient,
|
||||||
|
mockKubeManager,
|
||||||
|
&ScaleSettings{
|
||||||
|
Namespace: "namespace",
|
||||||
|
ResourceName: "resource",
|
||||||
|
MinRunners: 0,
|
||||||
|
MaxRunners: 5,
|
||||||
|
},
|
||||||
|
func(s *Service) {
|
||||||
|
s.logger = logger
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
err := service.processMessage(&actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "RunnerScaleSetJobMessages",
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{
|
||||||
|
TotalAvailableJobs: 1,
|
||||||
|
},
|
||||||
|
Body: "[\"something\", \"test\"]",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.ErrorContains(t, err, "could not decode job message type", "Unexpected error")
|
||||||
|
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessMessage_MultipleMessages(t *testing.T) {
|
||||||
|
mockRsClient := &MockRunnerScaleSetClient{}
|
||||||
|
mockKubeManager := &MockKubernetesManager{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
service := NewService(
|
||||||
|
ctx,
|
||||||
|
mockRsClient,
|
||||||
|
mockKubeManager,
|
||||||
|
&ScaleSettings{
|
||||||
|
Namespace: "namespace",
|
||||||
|
ResourceName: "resource",
|
||||||
|
MinRunners: 1,
|
||||||
|
MaxRunners: 5,
|
||||||
|
},
|
||||||
|
func(s *Service) {
|
||||||
|
s.logger = logger
|
||||||
|
},
|
||||||
|
)
|
||||||
|
mockRsClient.On("AcquireJobsForRunnerScaleSet", ctx, mock.MatchedBy(func(ids []int64) bool { return ids[0] == 3 && ids[1] == 4 })).Return(nil).Once()
|
||||||
|
mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 2).Run(func(args mock.Arguments) { cancel() }).Return(nil).Once()
|
||||||
|
|
||||||
|
err := service.processMessage(&actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "RunnerScaleSetJobMessages",
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{
|
||||||
|
TotalAssignedJobs: 2,
|
||||||
|
TotalAvailableJobs: 2,
|
||||||
|
},
|
||||||
|
Body: "[{\"messageType\":\"JobAvailable\", \"runnerRequestId\": 3},{\"messageType\":\"JobAvailable\", \"runnerRequestId\": 4},{\"messageType\":\"JobAssigned\", \"runnerRequestId\": 2}, {\"messageType\":\"JobCompleted\", \"runnerRequestId\": 1, \"result\":\"succeed\"},{\"messageType\":\"unknown\"}]",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err, "Unexpected error")
|
||||||
|
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessMessage_AcquireJobsFailed(t *testing.T) {
|
||||||
|
mockRsClient := &MockRunnerScaleSetClient{}
|
||||||
|
mockKubeManager := &MockKubernetesManager{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
service := NewService(
|
||||||
|
ctx,
|
||||||
|
mockRsClient,
|
||||||
|
mockKubeManager,
|
||||||
|
&ScaleSettings{
|
||||||
|
Namespace: "namespace",
|
||||||
|
ResourceName: "resource",
|
||||||
|
MinRunners: 0,
|
||||||
|
MaxRunners: 5,
|
||||||
|
},
|
||||||
|
func(s *Service) {
|
||||||
|
s.logger = logger
|
||||||
|
},
|
||||||
|
)
|
||||||
|
mockRsClient.On("AcquireJobsForRunnerScaleSet", ctx, mock.MatchedBy(func(ids []int64) bool { return ids[0] == 1 })).Return(fmt.Errorf("error")).Once()
|
||||||
|
|
||||||
|
err := service.processMessage(&actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "RunnerScaleSetJobMessages",
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{
|
||||||
|
TotalAssignedJobs: 1,
|
||||||
|
TotalAvailableJobs: 1,
|
||||||
|
},
|
||||||
|
Body: "[{\"messageType\":\"JobAvailable\", \"runnerRequestId\": 1}]",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.ErrorContains(t, err, "could not acquire jobs. error", "Unexpected error")
|
||||||
|
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScaleForAssignedJobCount_DeDupScale(t *testing.T) {
|
||||||
|
mockRsClient := &MockRunnerScaleSetClient{}
|
||||||
|
mockKubeManager := &MockKubernetesManager{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
service := NewService(
|
||||||
|
ctx,
|
||||||
|
mockRsClient,
|
||||||
|
mockKubeManager,
|
||||||
|
&ScaleSettings{
|
||||||
|
Namespace: "namespace",
|
||||||
|
ResourceName: "resource",
|
||||||
|
MinRunners: 0,
|
||||||
|
MaxRunners: 5,
|
||||||
|
},
|
||||||
|
func(s *Service) {
|
||||||
|
s.logger = logger
|
||||||
|
},
|
||||||
|
)
|
||||||
|
mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 2).Return(nil).Once()
|
||||||
|
|
||||||
|
err := service.scaleForAssignedJobCount(2)
|
||||||
|
require.NoError(t, err, "Unexpected error")
|
||||||
|
err = service.scaleForAssignedJobCount(2)
|
||||||
|
require.NoError(t, err, "Unexpected error")
|
||||||
|
err = service.scaleForAssignedJobCount(2)
|
||||||
|
require.NoError(t, err, "Unexpected error")
|
||||||
|
err = service.scaleForAssignedJobCount(2)
|
||||||
|
|
||||||
|
assert.NoError(t, err, "Unexpected error")
|
||||||
|
assert.Equal(t, 2, service.currentRunnerCount, "Unexpected runner count")
|
||||||
|
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScaleForAssignedJobCount_ScaleWithinMinMax(t *testing.T) {
|
||||||
|
mockRsClient := &MockRunnerScaleSetClient{}
|
||||||
|
mockKubeManager := &MockKubernetesManager{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
service := NewService(
|
||||||
|
ctx,
|
||||||
|
mockRsClient,
|
||||||
|
mockKubeManager,
|
||||||
|
&ScaleSettings{
|
||||||
|
Namespace: "namespace",
|
||||||
|
ResourceName: "resource",
|
||||||
|
MinRunners: 1,
|
||||||
|
MaxRunners: 5,
|
||||||
|
},
|
||||||
|
func(s *Service) {
|
||||||
|
s.logger = logger
|
||||||
|
},
|
||||||
|
)
|
||||||
|
mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 1).Return(nil).Once()
|
||||||
|
mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 3).Return(nil).Once()
|
||||||
|
mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 5).Return(nil).Once()
|
||||||
|
mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 1).Return(nil).Once()
|
||||||
|
mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 5).Return(nil).Once()
|
||||||
|
|
||||||
|
err := service.scaleForAssignedJobCount(0)
|
||||||
|
require.NoError(t, err, "Unexpected error")
|
||||||
|
err = service.scaleForAssignedJobCount(3)
|
||||||
|
require.NoError(t, err, "Unexpected error")
|
||||||
|
err = service.scaleForAssignedJobCount(5)
|
||||||
|
require.NoError(t, err, "Unexpected error")
|
||||||
|
err = service.scaleForAssignedJobCount(1)
|
||||||
|
require.NoError(t, err, "Unexpected error")
|
||||||
|
err = service.scaleForAssignedJobCount(10)
|
||||||
|
|
||||||
|
assert.NoError(t, err, "Unexpected error")
|
||||||
|
assert.Equal(t, 5, service.currentRunnerCount, "Unexpected runner count")
|
||||||
|
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScaleForAssignedJobCount_ScaleFailed(t *testing.T) {
|
||||||
|
mockRsClient := &MockRunnerScaleSetClient{}
|
||||||
|
mockKubeManager := &MockKubernetesManager{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
service := NewService(
|
||||||
|
ctx,
|
||||||
|
mockRsClient,
|
||||||
|
mockKubeManager,
|
||||||
|
&ScaleSettings{
|
||||||
|
Namespace: "namespace",
|
||||||
|
ResourceName: "resource",
|
||||||
|
MinRunners: 1,
|
||||||
|
MaxRunners: 5,
|
||||||
|
},
|
||||||
|
func(s *Service) {
|
||||||
|
s.logger = logger
|
||||||
|
},
|
||||||
|
)
|
||||||
|
mockKubeManager.On("ScaleEphemeralRunnerSet", ctx, service.settings.Namespace, service.settings.ResourceName, 2).Return(fmt.Errorf("error"))
|
||||||
|
|
||||||
|
err := service.scaleForAssignedJobCount(2)
|
||||||
|
|
||||||
|
assert.ErrorContains(t, err, "could not scale ephemeral runner set (namespace/resource). error", "Unexpected error")
|
||||||
|
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessMessage_JobStartedMessage(t *testing.T) {
|
||||||
|
mockRsClient := &MockRunnerScaleSetClient{}
|
||||||
|
mockKubeManager := &MockKubernetesManager{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
service := NewService(
|
||||||
|
ctx,
|
||||||
|
mockRsClient,
|
||||||
|
mockKubeManager,
|
||||||
|
&ScaleSettings{
|
||||||
|
Namespace: "namespace",
|
||||||
|
ResourceName: "resource",
|
||||||
|
MinRunners: 1,
|
||||||
|
MaxRunners: 5,
|
||||||
|
},
|
||||||
|
func(s *Service) {
|
||||||
|
s.logger = logger
|
||||||
|
},
|
||||||
|
)
|
||||||
|
service.currentRunnerCount = 1
|
||||||
|
|
||||||
|
mockKubeManager.On("UpdateEphemeralRunnerWithJobInfo", ctx, service.settings.Namespace, "runner1", "owner1", "repo1", ".github/workflows/ci.yaml", "job1", int64(100), int64(3)).Run(func(args mock.Arguments) { cancel() }).Return(nil).Once()
|
||||||
|
mockRsClient.On("AcquireJobsForRunnerScaleSet", ctx, mock.MatchedBy(func(ids []int64) bool { return len(ids) == 0 })).Return(nil).Once()
|
||||||
|
|
||||||
|
err := service.processMessage(&actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "RunnerScaleSetJobMessages",
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{
|
||||||
|
TotalAssignedJobs: 1,
|
||||||
|
TotalAvailableJobs: 0,
|
||||||
|
},
|
||||||
|
Body: "[{\"messageType\":\"JobStarted\", \"runnerRequestId\": 3, \"runnerId\": 1, \"runnerName\": \"runner1\", \"ownerName\": \"owner1\", \"repositoryName\": \"repo1\", \"jobWorkflowRef\": \".github/workflows/ci.yaml\", \"jobDisplayName\": \"job1\", \"workflowRunId\": 100 }]",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err, "Unexpected error")
|
||||||
|
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessMessage_JobStartedMessageIgnoreRunnerUpdateError(t *testing.T) {
|
||||||
|
mockRsClient := &MockRunnerScaleSetClient{}
|
||||||
|
mockKubeManager := &MockKubernetesManager{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
service := NewService(
|
||||||
|
ctx,
|
||||||
|
mockRsClient,
|
||||||
|
mockKubeManager,
|
||||||
|
&ScaleSettings{
|
||||||
|
Namespace: "namespace",
|
||||||
|
ResourceName: "resource",
|
||||||
|
MinRunners: 1,
|
||||||
|
MaxRunners: 5,
|
||||||
|
},
|
||||||
|
func(s *Service) {
|
||||||
|
s.logger = logger
|
||||||
|
},
|
||||||
|
)
|
||||||
|
service.currentRunnerCount = 1
|
||||||
|
|
||||||
|
mockKubeManager.On("UpdateEphemeralRunnerWithJobInfo", ctx, service.settings.Namespace, "runner1", "owner1", "repo1", ".github/workflows/ci.yaml", "job1", int64(100), int64(3)).Run(func(args mock.Arguments) { cancel() }).Return(fmt.Errorf("error")).Once()
|
||||||
|
mockRsClient.On("AcquireJobsForRunnerScaleSet", ctx, mock.MatchedBy(func(ids []int64) bool { return len(ids) == 0 })).Return(nil).Once()
|
||||||
|
|
||||||
|
err := service.processMessage(&actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "RunnerScaleSetJobMessages",
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{
|
||||||
|
TotalAssignedJobs: 0,
|
||||||
|
TotalAvailableJobs: 0,
|
||||||
|
},
|
||||||
|
Body: "[{\"messageType\":\"JobStarted\", \"runnerRequestId\": 3, \"runnerId\": 1, \"runnerName\": \"runner1\", \"ownerName\": \"owner1\", \"repositoryName\": \"repo1\", \"jobWorkflowRef\": \".github/workflows/ci.yaml\", \"jobDisplayName\": \"job1\", \"workflowRunId\": 100 }]",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err, "Unexpected error")
|
||||||
|
assert.True(t, mockRsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
assert.True(t, mockKubeManager.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
12
cmd/githubrunnerscalesetlistener/kubernetesManager.go
Normal file
12
cmd/githubrunnerscalesetlistener/kubernetesManager.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate mockery --inpackage --name=KubernetesManager
|
||||||
|
type KubernetesManager interface {
|
||||||
|
ScaleEphemeralRunnerSet(ctx context.Context, namespace, resourceName string, runnerCount int) error
|
||||||
|
|
||||||
|
UpdateEphemeralRunnerWithJobInfo(ctx context.Context, namespace, resourceName, ownerName, repositoryName, jobWorkflowRef, jobDisplayName string, jobRequestId, workflowRunId int64) error
|
||||||
|
}
|
||||||
151
cmd/githubrunnerscalesetlistener/main.go
Normal file
151
cmd/githubrunnerscalesetlistener/main.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The actions-runner-controller authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
"github.com/actions/actions-runner-controller/logging"
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
"github.com/kelseyhightower/envconfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RunnerScaleSetListenerConfig struct {
|
||||||
|
ConfigureUrl string `split_words:"true"`
|
||||||
|
AppID int64 `split_words:"true"`
|
||||||
|
AppInstallationID int64 `split_words:"true"`
|
||||||
|
AppPrivateKey string `split_words:"true"`
|
||||||
|
Token string `split_words:"true"`
|
||||||
|
EphemeralRunnerSetNamespace string `split_words:"true"`
|
||||||
|
EphemeralRunnerSetName string `split_words:"true"`
|
||||||
|
MaxRunners int `split_words:"true"`
|
||||||
|
MinRunners int `split_words:"true"`
|
||||||
|
RunnerScaleSetId int `split_words:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: creating logger: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rc RunnerScaleSetListenerConfig
|
||||||
|
if err := envconfig.Process("github", &rc); err != nil {
|
||||||
|
logger.Error(err, "Error: processing environment variables for RunnerScaleSetListenerConfig")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate all inputs
|
||||||
|
if err := validateConfig(&rc); err != nil {
|
||||||
|
logger.Error(err, "Inputs validation failed")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := run(rc, logger); err != nil {
|
||||||
|
logger.Error(err, "Run error")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(rc RunnerScaleSetListenerConfig, logger logr.Logger) error {
|
||||||
|
// Create root context and hook with sigint and sigterm
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
creds := &actions.ActionsAuth{}
|
||||||
|
if rc.Token != "" {
|
||||||
|
creds.Token = rc.Token
|
||||||
|
} else {
|
||||||
|
creds.AppCreds = &actions.GitHubAppAuth{
|
||||||
|
AppID: rc.AppID,
|
||||||
|
AppInstallationID: rc.AppInstallationID,
|
||||||
|
AppPrivateKey: rc.AppPrivateKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actionsServiceClient, err := actions.NewClient(ctx, rc.ConfigureUrl, creds, "actions-runner-controller", logger)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create an Actions Service client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create message listener
|
||||||
|
autoScalerClient, err := NewAutoScalerClient(ctx, actionsServiceClient, &logger, rc.RunnerScaleSetId)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create a message listener: %w", err)
|
||||||
|
}
|
||||||
|
defer autoScalerClient.Close()
|
||||||
|
|
||||||
|
// Create kube manager and scale controller
|
||||||
|
kubeManager, err := NewKubernetesManager(&logger)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create kubernetes manager: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleSettings := &ScaleSettings{
|
||||||
|
Namespace: rc.EphemeralRunnerSetNamespace,
|
||||||
|
ResourceName: rc.EphemeralRunnerSetName,
|
||||||
|
MaxRunners: rc.MaxRunners,
|
||||||
|
MinRunners: rc.MinRunners,
|
||||||
|
}
|
||||||
|
|
||||||
|
service := NewService(ctx, autoScalerClient, kubeManager, scaleSettings, func(s *Service) {
|
||||||
|
s.logger = logger.WithName("service")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start listening for messages
|
||||||
|
if err = service.Start(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start message queue listener: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateConfig(config *RunnerScaleSetListenerConfig) error {
|
||||||
|
if len(config.ConfigureUrl) == 0 {
|
||||||
|
return fmt.Errorf("GitHubConfigUrl is not provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.EphemeralRunnerSetNamespace) == 0 || len(config.EphemeralRunnerSetName) == 0 {
|
||||||
|
return fmt.Errorf("EphemeralRunnerSetNamespace '%s' or EphemeralRunnerSetName '%s' is missing", config.EphemeralRunnerSetNamespace, config.EphemeralRunnerSetName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.RunnerScaleSetId == 0 {
|
||||||
|
return fmt.Errorf("RunnerScaleSetId '%d' is missing", config.RunnerScaleSetId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.MaxRunners < config.MinRunners {
|
||||||
|
return fmt.Errorf("MinRunners '%d' cannot be greater than MaxRunners '%d'", config.MinRunners, config.MaxRunners)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasToken := len(config.Token) > 0
|
||||||
|
hasPrivateKeyConfig := config.AppID > 0 && config.AppPrivateKey != ""
|
||||||
|
|
||||||
|
if !hasToken && !hasPrivateKeyConfig {
|
||||||
|
return fmt.Errorf("GitHub auth credential is missing, token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasToken && hasPrivateKeyConfig {
|
||||||
|
return fmt.Errorf("only one GitHub auth method supported at a time. Have both PAT and App auth: token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
92
cmd/githubrunnerscalesetlistener/main_test.go
Normal file
92
cmd/githubrunnerscalesetlistener/main_test.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigValidationMinMax(t *testing.T) {
|
||||||
|
config := &RunnerScaleSetListenerConfig{
|
||||||
|
ConfigureUrl: "github.com/some_org/some_repo",
|
||||||
|
EphemeralRunnerSetNamespace: "namespace",
|
||||||
|
EphemeralRunnerSetName: "deployment",
|
||||||
|
RunnerScaleSetId: 1,
|
||||||
|
MinRunners: 5,
|
||||||
|
MaxRunners: 2,
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
err := validateConfig(config)
|
||||||
|
assert.ErrorContains(t, err, "MinRunners '5' cannot be greater than MaxRunners '2", "Expected error about MinRunners > MaxRunners")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigValidationMissingToken(t *testing.T) {
|
||||||
|
config := &RunnerScaleSetListenerConfig{
|
||||||
|
ConfigureUrl: "github.com/some_org/some_repo",
|
||||||
|
EphemeralRunnerSetNamespace: "namespace",
|
||||||
|
EphemeralRunnerSetName: "deployment",
|
||||||
|
RunnerScaleSetId: 1,
|
||||||
|
}
|
||||||
|
err := validateConfig(config)
|
||||||
|
expectedError := fmt.Sprintf("GitHub auth credential is missing, token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey))
|
||||||
|
assert.ErrorContains(t, err, expectedError, "Expected error about missing auth")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigValidationAppKey(t *testing.T) {
|
||||||
|
config := &RunnerScaleSetListenerConfig{
|
||||||
|
AppID: 1,
|
||||||
|
AppInstallationID: 10,
|
||||||
|
ConfigureUrl: "github.com/some_org/some_repo",
|
||||||
|
EphemeralRunnerSetNamespace: "namespace",
|
||||||
|
EphemeralRunnerSetName: "deployment",
|
||||||
|
RunnerScaleSetId: 1,
|
||||||
|
}
|
||||||
|
err := validateConfig(config)
|
||||||
|
expectedError := fmt.Sprintf("GitHub auth credential is missing, token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey))
|
||||||
|
assert.ErrorContains(t, err, expectedError, "Expected error about missing auth")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigValidationOnlyOneTypeOfCredentials(t *testing.T) {
|
||||||
|
config := &RunnerScaleSetListenerConfig{
|
||||||
|
AppID: 1,
|
||||||
|
AppInstallationID: 10,
|
||||||
|
AppPrivateKey: "asdf",
|
||||||
|
Token: "asdf",
|
||||||
|
ConfigureUrl: "github.com/some_org/some_repo",
|
||||||
|
EphemeralRunnerSetNamespace: "namespace",
|
||||||
|
EphemeralRunnerSetName: "deployment",
|
||||||
|
RunnerScaleSetId: 1,
|
||||||
|
}
|
||||||
|
err := validateConfig(config)
|
||||||
|
expectedError := fmt.Sprintf("only one GitHub auth method supported at a time. Have both PAT and App auth: token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey))
|
||||||
|
assert.ErrorContains(t, err, expectedError, "Expected error about missing auth")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigValidation(t *testing.T) {
|
||||||
|
config := &RunnerScaleSetListenerConfig{
|
||||||
|
ConfigureUrl: "https://github.com/actions",
|
||||||
|
EphemeralRunnerSetNamespace: "namespace",
|
||||||
|
EphemeralRunnerSetName: "deployment",
|
||||||
|
RunnerScaleSetId: 1,
|
||||||
|
MinRunners: 1,
|
||||||
|
MaxRunners: 5,
|
||||||
|
Token: "asdf",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := validateConfig(config)
|
||||||
|
|
||||||
|
assert.NoError(t, err, "Expected no error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigValidationConfigUrl(t *testing.T) {
|
||||||
|
config := &RunnerScaleSetListenerConfig{
|
||||||
|
EphemeralRunnerSetNamespace: "namespace",
|
||||||
|
EphemeralRunnerSetName: "deployment",
|
||||||
|
RunnerScaleSetId: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := validateConfig(config)
|
||||||
|
|
||||||
|
assert.ErrorContains(t, err, "GitHubConfigUrl is not provided", "Expected error about missing ConfigureUrl")
|
||||||
|
}
|
||||||
13
cmd/githubrunnerscalesetlistener/messageListener.go
Normal file
13
cmd/githubrunnerscalesetlistener/messageListener.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate mockery --inpackage --name=RunnerScaleSetClient
|
||||||
|
type RunnerScaleSetClient interface {
|
||||||
|
GetRunnerScaleSetMessage(ctx context.Context, handler func(msg *actions.RunnerScaleSetMessage) error) error
|
||||||
|
AcquireJobsForRunnerScaleSet(ctx context.Context, requestIds []int64) error
|
||||||
|
}
|
||||||
57
cmd/githubrunnerscalesetlistener/mock_KubernetesManager.go
Normal file
57
cmd/githubrunnerscalesetlistener/mock_KubernetesManager.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// Code generated by mockery v2.16.0. DO NOT EDIT.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
|
||||||
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockKubernetesManager is an autogenerated mock type for the KubernetesManager type
|
||||||
|
type MockKubernetesManager struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScaleEphemeralRunnerSet provides a mock function with given fields: ctx, namespace, resourceName, runnerCount
|
||||||
|
func (_m *MockKubernetesManager) ScaleEphemeralRunnerSet(ctx context.Context, namespace string, resourceName string, runnerCount int) error {
|
||||||
|
ret := _m.Called(ctx, namespace, resourceName, runnerCount)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string, string, int) error); ok {
|
||||||
|
r0 = rf(ctx, namespace, resourceName, runnerCount)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateEphemeralRunnerWithJobInfo provides a mock function with given fields: ctx, namespace, resourceName, ownerName, repositoryName, jobWorkflowRef, jobDisplayName, jobRequestId, workflowRunId
|
||||||
|
func (_m *MockKubernetesManager) UpdateEphemeralRunnerWithJobInfo(ctx context.Context, namespace string, resourceName string, ownerName string, repositoryName string, jobWorkflowRef string, jobDisplayName string, jobRequestId int64, workflowRunId int64) error {
|
||||||
|
ret := _m.Called(ctx, namespace, resourceName, ownerName, repositoryName, jobWorkflowRef, jobDisplayName, jobRequestId, workflowRunId)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, string, string, int64, int64) error); ok {
|
||||||
|
r0 = rf(ctx, namespace, resourceName, ownerName, repositoryName, jobWorkflowRef, jobDisplayName, jobRequestId, workflowRunId)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockConstructorTestingTNewMockKubernetesManager interface {
|
||||||
|
mock.TestingT
|
||||||
|
Cleanup(func())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockKubernetesManager creates a new instance of MockKubernetesManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||||
|
func NewMockKubernetesManager(t mockConstructorTestingTNewMockKubernetesManager) *MockKubernetesManager {
|
||||||
|
mock := &MockKubernetesManager{}
|
||||||
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// Code generated by mockery v2.16.0. DO NOT EDIT.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
|
||||||
|
actions "github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
|
||||||
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockRunnerScaleSetClient is an autogenerated mock type for the RunnerScaleSetClient type
|
||||||
|
type MockRunnerScaleSetClient struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcquireJobsForRunnerScaleSet provides a mock function with given fields: ctx, requestIds
|
||||||
|
func (_m *MockRunnerScaleSetClient) AcquireJobsForRunnerScaleSet(ctx context.Context, requestIds []int64) error {
|
||||||
|
ret := _m.Called(ctx, requestIds)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, []int64) error); ok {
|
||||||
|
r0 = rf(ctx, requestIds)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRunnerScaleSetMessage provides a mock function with given fields: ctx, handler
|
||||||
|
func (_m *MockRunnerScaleSetClient) GetRunnerScaleSetMessage(ctx context.Context, handler func(*actions.RunnerScaleSetMessage) error) error {
|
||||||
|
ret := _m.Called(ctx, handler)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, func(*actions.RunnerScaleSetMessage) error) error); ok {
|
||||||
|
r0 = rf(ctx, handler)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockConstructorTestingTNewMockRunnerScaleSetClient interface {
|
||||||
|
mock.TestingT
|
||||||
|
Cleanup(func())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockRunnerScaleSetClient creates a new instance of MockRunnerScaleSetClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||||
|
func NewMockRunnerScaleSetClient(t mockConstructorTestingTNewMockRunnerScaleSetClient) *MockRunnerScaleSetClient {
|
||||||
|
mock := &MockRunnerScaleSetClient{}
|
||||||
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
123
cmd/githubrunnerscalesetlistener/sessionrefreshingclient.go
Normal file
123
cmd/githubrunnerscalesetlistener/sessionrefreshingclient.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionRefreshingClient struct {
|
||||||
|
client actions.ActionsService
|
||||||
|
logger logr.Logger
|
||||||
|
session *actions.RunnerScaleSetSession
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSessionClient(client actions.ActionsService, logger *logr.Logger, session *actions.RunnerScaleSetSession) *SessionRefreshingClient {
|
||||||
|
return &SessionRefreshingClient{
|
||||||
|
client: client,
|
||||||
|
session: session,
|
||||||
|
logger: logger.WithName("refreshing_client"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SessionRefreshingClient) GetMessage(ctx context.Context, lastMessageId int64) (*actions.RunnerScaleSetMessage, error) {
|
||||||
|
message, err := m.client.GetMessage(ctx, m.session.MessageQueueUrl, m.session.MessageQueueAccessToken, lastMessageId)
|
||||||
|
if err == nil {
|
||||||
|
return message, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
expiredError := &actions.MessageQueueTokenExpiredError{}
|
||||||
|
if !errors.As(err, &expiredError) {
|
||||||
|
return nil, fmt.Errorf("get message failed. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.logger.Info("message queue token is expired during GetNextMessage, refreshing...")
|
||||||
|
session, err := m.client.RefreshMessageSession(ctx, m.session.RunnerScaleSet.Id, m.session.SessionId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("refresh message session failed. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.session = session
|
||||||
|
message, err = m.client.GetMessage(ctx, m.session.MessageQueueUrl, m.session.MessageQueueAccessToken, lastMessageId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("delete message failed after refresh message session. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return message, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SessionRefreshingClient) DeleteMessage(ctx context.Context, messageId int64) error {
|
||||||
|
err := m.client.DeleteMessage(ctx, m.session.MessageQueueUrl, m.session.MessageQueueAccessToken, messageId)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
expiredError := &actions.MessageQueueTokenExpiredError{}
|
||||||
|
if !errors.As(err, &expiredError) {
|
||||||
|
return fmt.Errorf("delete message failed. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.logger.Info("message queue token is expired during DeleteMessage, refreshing...")
|
||||||
|
session, err := m.client.RefreshMessageSession(ctx, m.session.RunnerScaleSet.Id, m.session.SessionId)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("refresh message session failed. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.session = session
|
||||||
|
err = m.client.DeleteMessage(ctx, m.session.MessageQueueUrl, m.session.MessageQueueAccessToken, messageId)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("delete message failed after refresh message session. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SessionRefreshingClient) AcquireJobs(ctx context.Context, requestIds []int64) ([]int64, error) {
|
||||||
|
ids, err := m.client.AcquireJobs(ctx, m.session.RunnerScaleSet.Id, m.session.MessageQueueAccessToken, requestIds)
|
||||||
|
if err == nil {
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
expiredError := &actions.MessageQueueTokenExpiredError{}
|
||||||
|
if !errors.As(err, &expiredError) {
|
||||||
|
return nil, fmt.Errorf("acquire jobs failed. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.logger.Info("message queue token is expired during AcquireJobs, refreshing...")
|
||||||
|
session, err := m.client.RefreshMessageSession(ctx, m.session.RunnerScaleSet.Id, m.session.SessionId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("refresh message session failed. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.session = session
|
||||||
|
ids, err = m.client.AcquireJobs(ctx, m.session.RunnerScaleSet.Id, m.session.MessageQueueAccessToken, requestIds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("acquire jobs failed after refresh message session. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SessionRefreshingClient) Close() error {
|
||||||
|
if m.session == nil {
|
||||||
|
m.logger.Info("session is already deleted. (no-op)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxWithTimeout, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
m.logger.Info("deleting session.")
|
||||||
|
err := m.client.DeleteMessageSession(ctxWithTimeout, m.session.RunnerScaleSet.Id, m.session.SessionId)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("delete message session failed. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.session = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
421
cmd/githubrunnerscalesetlistener/sessionrefreshingclient_test.go
Normal file
421
cmd/githubrunnerscalesetlistener/sessionrefreshingclient_test.go
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
"github.com/actions/actions-runner-controller/logging"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetMessage(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockActionsClient.On("GetMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(0)).Return(nil, nil).Once()
|
||||||
|
mockActionsClient.On("GetMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(0)).Return(&actions.RunnerScaleSetMessage{MessageId: 1}, nil).Once()
|
||||||
|
|
||||||
|
client := newSessionClient(mockActionsClient, &logger, session)
|
||||||
|
|
||||||
|
msg, err := client.GetMessage(ctx, 0)
|
||||||
|
require.NoError(t, err, "GetMessage should not return an error")
|
||||||
|
|
||||||
|
assert.Nil(t, msg, "GetMessage should return nil message")
|
||||||
|
|
||||||
|
msg, err = client.GetMessage(ctx, 0)
|
||||||
|
require.NoError(t, err, "GetMessage should not return an error")
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1), msg.MessageId, "GetMessage should return a message with id 1")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expected calls to mockActionsClient should have been made")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteMessage(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockActionsClient.On("DeleteMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(1)).Return(nil).Once()
|
||||||
|
|
||||||
|
client := newSessionClient(mockActionsClient, &logger, session)
|
||||||
|
|
||||||
|
err := client.DeleteMessage(ctx, int64(1))
|
||||||
|
assert.NoError(t, err, "DeleteMessage should not return an error")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expected calls to mockActionsClient should have been made")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAcquireJobs(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("AcquireJobs", ctx, mock.Anything, "token", mock.MatchedBy(func(ids []int64) bool { return ids[0] == 1 && ids[1] == 2 && ids[2] == 3 })).Return([]int64{1}, nil)
|
||||||
|
|
||||||
|
client := newSessionClient(mockActionsClient, &logger, session)
|
||||||
|
|
||||||
|
ids, err := client.AcquireJobs(ctx, []int64{1, 2, 3})
|
||||||
|
assert.NoError(t, err, "AcquireJobs should not return an error")
|
||||||
|
assert.Equal(t, []int64{1}, ids, "AcquireJobs should return a slice with one id")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expected calls to mockActionsClient should have been made")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClose(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockActionsClient.On("DeleteMessageSession", mock.Anything, 1, &sessionId).Return(nil).Once()
|
||||||
|
|
||||||
|
client := newSessionClient(mockActionsClient, &logger, session)
|
||||||
|
|
||||||
|
err := client.Close()
|
||||||
|
assert.NoError(t, err, "DeleteMessageSession should not return an error")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expected calls to mockActionsClient should have been made")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetMessage_Error(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockActionsClient.On("GetMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(0)).Return(nil, fmt.Errorf("error")).Once()
|
||||||
|
|
||||||
|
client := newSessionClient(mockActionsClient, &logger, session)
|
||||||
|
|
||||||
|
msg, err := client.GetMessage(ctx, 0)
|
||||||
|
assert.ErrorContains(t, err, "get message failed. error", "GetMessage should return an error")
|
||||||
|
assert.Nil(t, msg, "GetMessage should return nil message")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expected calls to mockActionsClient should have been made")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteMessage_SessionError(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockActionsClient.On("DeleteMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(1)).Return(fmt.Errorf("error")).Once()
|
||||||
|
|
||||||
|
client := newSessionClient(mockActionsClient, &logger, session)
|
||||||
|
|
||||||
|
err := client.DeleteMessage(ctx, int64(1))
|
||||||
|
assert.ErrorContains(t, err, "delete message failed. error", "DeleteMessage should return an error")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expected calls to mockActionsClient should have been made")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAcquireJobs_Error(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("AcquireJobs", ctx, mock.Anything, "token", mock.MatchedBy(func(ids []int64) bool { return ids[0] == 1 && ids[1] == 2 && ids[2] == 3 })).Return(nil, fmt.Errorf("error")).Once()
|
||||||
|
|
||||||
|
client := newSessionClient(mockActionsClient, &logger, session)
|
||||||
|
|
||||||
|
ids, err := client.AcquireJobs(ctx, []int64{1, 2, 3})
|
||||||
|
assert.ErrorContains(t, err, "acquire jobs failed. error", "AcquireJobs should return an error")
|
||||||
|
assert.Nil(t, ids, "AcquireJobs should return nil ids")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expected calls to mockActionsClient should have been made")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetMessage_RefreshToken(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("GetMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(0)).Return(nil, &actions.MessageQueueTokenExpiredError{}).Once()
|
||||||
|
mockActionsClient.On("GetMessage", ctx, session.MessageQueueUrl, "token2", int64(0)).Return(&actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "test",
|
||||||
|
Body: "test",
|
||||||
|
}, nil).Once()
|
||||||
|
mockActionsClient.On("RefreshMessageSession", ctx, session.RunnerScaleSet.Id, session.SessionId).Return(&actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token2",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
}, nil).Once()
|
||||||
|
|
||||||
|
client := newSessionClient(mockActionsClient, &logger, session)
|
||||||
|
msg, err := client.GetMessage(ctx, 0)
|
||||||
|
assert.NoError(t, err, "Error getting message")
|
||||||
|
assert.Equal(t, int64(1), msg.MessageId, "message id should be updated")
|
||||||
|
assert.Equal(t, "token2", client.session.MessageQueueAccessToken, "Message queue access token should be updated")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteMessage_RefreshSessionToken(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockActionsClient.On("DeleteMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(1)).Return(&actions.MessageQueueTokenExpiredError{}).Once()
|
||||||
|
mockActionsClient.On("DeleteMessage", ctx, session.MessageQueueUrl, "token2", int64(1)).Return(nil).Once()
|
||||||
|
mockActionsClient.On("RefreshMessageSession", ctx, session.RunnerScaleSet.Id, session.SessionId).Return(&actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token2",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
client := newSessionClient(mockActionsClient, &logger, session)
|
||||||
|
err := client.DeleteMessage(ctx, 1)
|
||||||
|
assert.NoError(t, err, "Error delete message")
|
||||||
|
assert.Equal(t, "token2", client.session.MessageQueueAccessToken, "Message queue access token should be updated")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAcquireJobs_RefreshToken(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockActionsClient.On("AcquireJobs", ctx, mock.Anything, session.MessageQueueAccessToken, mock.MatchedBy(func(ids []int64) bool { return ids[0] == 1 && ids[1] == 2 && ids[2] == 3 })).Return(nil, &actions.MessageQueueTokenExpiredError{}).Once()
|
||||||
|
mockActionsClient.On("AcquireJobs", ctx, mock.Anything, "token2", mock.MatchedBy(func(ids []int64) bool { return ids[0] == 1 && ids[1] == 2 && ids[2] == 3 })).Return([]int64{1, 2, 3}, nil)
|
||||||
|
mockActionsClient.On("RefreshMessageSession", ctx, session.RunnerScaleSet.Id, session.SessionId).Return(&actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token2",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
client := newSessionClient(mockActionsClient, &logger, session)
|
||||||
|
ids, err := client.AcquireJobs(ctx, []int64{1, 2, 3})
|
||||||
|
assert.NoError(t, err, "Error acquiring jobs")
|
||||||
|
assert.Equal(t, []int64{1, 2, 3}, ids, "Job ids should be returned")
|
||||||
|
assert.Equal(t, "token2", client.session.MessageQueueAccessToken, "Message queue access token should be updated")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetMessage_RefreshToken_Failed(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("GetMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(0)).Return(nil, &actions.MessageQueueTokenExpiredError{}).Once()
|
||||||
|
mockActionsClient.On("RefreshMessageSession", ctx, session.RunnerScaleSet.Id, session.SessionId).Return(nil, fmt.Errorf("error"))
|
||||||
|
|
||||||
|
client := newSessionClient(mockActionsClient, &logger, session)
|
||||||
|
msg, err := client.GetMessage(ctx, 0)
|
||||||
|
assert.ErrorContains(t, err, "refresh message session failed. error", "Error should be returned")
|
||||||
|
assert.Nil(t, msg, "Message should be nil")
|
||||||
|
assert.Equal(t, "token", client.session.MessageQueueAccessToken, "Message queue access token should not be updated")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteMessage_RefreshToken_Failed(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockActionsClient.On("DeleteMessage", ctx, session.MessageQueueUrl, session.MessageQueueAccessToken, int64(1)).Return(&actions.MessageQueueTokenExpiredError{}).Once()
|
||||||
|
mockActionsClient.On("RefreshMessageSession", ctx, session.RunnerScaleSet.Id, session.SessionId).Return(nil, fmt.Errorf("error"))
|
||||||
|
|
||||||
|
client := newSessionClient(mockActionsClient, &logger, session)
|
||||||
|
err := client.DeleteMessage(ctx, 1)
|
||||||
|
|
||||||
|
assert.ErrorContains(t, err, "refresh message session failed. error", "Error getting message")
|
||||||
|
assert.Equal(t, "token", client.session.MessageQueueAccessToken, "Message queue access token should not be updated")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAcquireJobs_RefreshToken_Failed(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sessionId := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionId,
|
||||||
|
OwnerName: "owner",
|
||||||
|
MessageQueueUrl: "https://github.com",
|
||||||
|
MessageQueueAccessToken: "token",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockActionsClient.On("AcquireJobs", ctx, mock.Anything, session.MessageQueueAccessToken, mock.MatchedBy(func(ids []int64) bool { return ids[0] == 1 && ids[1] == 2 && ids[2] == 3 })).Return(nil, &actions.MessageQueueTokenExpiredError{}).Once()
|
||||||
|
mockActionsClient.On("RefreshMessageSession", ctx, session.RunnerScaleSet.Id, session.SessionId).Return(nil, fmt.Errorf("error"))
|
||||||
|
|
||||||
|
client := newSessionClient(mockActionsClient, &logger, session)
|
||||||
|
ids, err := client.AcquireJobs(ctx, []int64{1, 2, 3})
|
||||||
|
assert.ErrorContains(t, err, "refresh message session failed. error", "Expect error refreshing message session")
|
||||||
|
assert.Nil(t, ids, "Job ids should be nil")
|
||||||
|
assert.Equal(t, "token", client.session.MessageQueueAccessToken, "Message queue access token should not be updated")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClose_Skip(t *testing.T) {
|
||||||
|
mockActionsClient := &actions.MockActionsService{}
|
||||||
|
logger, log_err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
logger = logger.WithName(t.Name())
|
||||||
|
require.NoError(t, log_err, "Error creating logger")
|
||||||
|
|
||||||
|
client := newSessionClient(mockActionsClient, &logger, nil)
|
||||||
|
err := client.Close()
|
||||||
|
require.NoError(t, err, "Error closing session client")
|
||||||
|
assert.True(t, mockActionsClient.AssertExpectations(t), "All expectations should be met")
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.7.0
|
||||||
|
creationTimestamp: null
|
||||||
|
name: autoscalinglisteners.actions.github.com
|
||||||
|
spec:
|
||||||
|
group: actions.github.com
|
||||||
|
names:
|
||||||
|
kind: AutoscalingListener
|
||||||
|
listKind: AutoscalingListenerList
|
||||||
|
plural: autoscalinglisteners
|
||||||
|
singular: autoscalinglistener
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- additionalPrinterColumns:
|
||||||
|
- jsonPath: .spec.githubConfigUrl
|
||||||
|
name: GitHub Configure URL
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.autoscalingRunnerSetNamespace
|
||||||
|
name: AutoscalingRunnerSet Namespace
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.autoscalingRunnerSetName
|
||||||
|
name: AutoscalingRunnerSet Name
|
||||||
|
type: string
|
||||||
|
name: v1alpha1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: AutoscalingListener is the Schema for the autoscalinglisteners API
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||||
|
type: string
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
spec:
|
||||||
|
description: AutoscalingListenerSpec defines the desired state of AutoscalingListener
|
||||||
|
properties:
|
||||||
|
autoscalingRunnerSetName:
|
||||||
|
description: Required
|
||||||
|
type: string
|
||||||
|
autoscalingRunnerSetNamespace:
|
||||||
|
description: Required
|
||||||
|
type: string
|
||||||
|
ephemeralRunnerSetName:
|
||||||
|
description: Required
|
||||||
|
type: string
|
||||||
|
githubConfigSecret:
|
||||||
|
description: Required
|
||||||
|
type: string
|
||||||
|
githubConfigUrl:
|
||||||
|
description: Required
|
||||||
|
type: string
|
||||||
|
image:
|
||||||
|
description: Required
|
||||||
|
type: string
|
||||||
|
imagePullSecrets:
|
||||||
|
description: Required
|
||||||
|
items:
|
||||||
|
description: LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?'
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
maxRunners:
|
||||||
|
description: Required
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
minRunners:
|
||||||
|
description: Required
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
runnerScaleSetId:
|
||||||
|
description: Required
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
description: AutoscalingListenerStatus defines the observed state of AutoscalingListener
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
|
preserveUnknownFields: false
|
||||||
|
status:
|
||||||
|
acceptedNames:
|
||||||
|
kind: ""
|
||||||
|
plural: ""
|
||||||
|
conditions: []
|
||||||
|
storedVersions: []
|
||||||
4218
config/crd/bases/actions.github.com_autoscalingrunnersets.yaml
Normal file
4218
config/crd/bases/actions.github.com_autoscalingrunnersets.yaml
Normal file
File diff suppressed because it is too large
Load Diff
4249
config/crd/bases/actions.github.com_ephemeralrunners.yaml
Normal file
4249
config/crd/bases/actions.github.com_ephemeralrunners.yaml
Normal file
File diff suppressed because it is too large
Load Diff
4206
config/crd/bases/actions.github.com_ephemeralrunnersets.yaml
Normal file
4206
config/crd/bases/actions.github.com_ephemeralrunnersets.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,10 @@ resources:
|
|||||||
- bases/actions.summerwind.dev_runnerdeployments.yaml
|
- bases/actions.summerwind.dev_runnerdeployments.yaml
|
||||||
- bases/actions.summerwind.dev_horizontalrunnerautoscalers.yaml
|
- bases/actions.summerwind.dev_horizontalrunnerautoscalers.yaml
|
||||||
- bases/actions.summerwind.dev_runnersets.yaml
|
- bases/actions.summerwind.dev_runnersets.yaml
|
||||||
|
- bases/actions.github.com_autoscalingrunnersets.yaml
|
||||||
|
- bases/actions.github.com_ephemeralrunners.yaml
|
||||||
|
- bases/actions.github.com_ephemeralrunnersets.yaml
|
||||||
|
- bases/actions.github.com_autoscalinglisteners.yaml
|
||||||
# +kubebuilder:scaffold:crdkustomizeresource
|
# +kubebuilder:scaffold:crdkustomizeresource
|
||||||
|
|
||||||
patchesStrategicMerge:
|
patchesStrategicMerge:
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ spec:
|
|||||||
optional: true
|
optional: true
|
||||||
- name: GITHUB_APP_PRIVATE_KEY
|
- name: GITHUB_APP_PRIVATE_KEY
|
||||||
value: /etc/actions-runner-controller/github_app_private_key
|
value: /etc/actions-runner-controller/github_app_private_key
|
||||||
|
- name: CONTROLLER_MANAGER_POD_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.name
|
||||||
|
- name: CONTROLLER_MANAGER_POD_NAMESPACE
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.namespace
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: controller-manager
|
- name: controller-manager
|
||||||
mountPath: "/etc/actions-runner-controller"
|
mountPath: "/etc/actions-runner-controller"
|
||||||
|
|||||||
24
config/rbac/autoscalinglistener_editor_role.yaml
Normal file
24
config/rbac/autoscalinglistener_editor_role.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# permissions for end users to edit autoscalinglisteners.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: autoscalinglistener-editor-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- autoscalinglisteners
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- autoscalinglisteners/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
20
config/rbac/autoscalinglistener_viewer_role.yaml
Normal file
20
config/rbac/autoscalinglistener_viewer_role.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# permissions for end users to view autoscalinglisteners.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: autoscalinglistener-viewer-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- autoscalinglisteners
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- autoscalinglisteners/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
24
config/rbac/autoscalingrunnerset_editor_role.yaml
Normal file
24
config/rbac/autoscalingrunnerset_editor_role.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# permissions for end users to edit autoscalingrunnersets.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: autoscalingrunnerset-editor-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- autoscalingrunnersets
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- autoscalingrunnersets/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
20
config/rbac/autoscalingrunnerset_viewer_role.yaml
Normal file
20
config/rbac/autoscalingrunnerset_viewer_role.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# permissions for end users to view autoscalingrunnersets.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: autoscalingrunnerset-viewer-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- autoscalingrunnersets
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- autoscalingrunnersets/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
24
config/rbac/ephemeralrunner_editor_role.yaml
Normal file
24
config/rbac/ephemeralrunner_editor_role.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# permissions for end users to edit ephemeralrunners.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: ephemeralrunner-editor-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- ephemeralrunners
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- ephemeralrunners/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
20
config/rbac/ephemeralrunner_viewer_role.yaml
Normal file
20
config/rbac/ephemeralrunner_viewer_role.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# permissions for end users to view ephemeralrunners.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: ephemeralrunner-viewer-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- ephemeralrunners
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- ephemeralrunners/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
24
config/rbac/ephemeralrunnerset_editor_role.yaml
Normal file
24
config/rbac/ephemeralrunnerset_editor_role.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# permissions for end users to edit ephemeralrunnersets.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: ephemeralrunnerset-editor-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- ephemeralrunnersets
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- ephemeralrunnersets/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
20
config/rbac/ephemeralrunnerset_viewer_role.yaml
Normal file
20
config/rbac/ephemeralrunnerset_viewer_role.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# permissions for end users to view ephemeralrunnersets.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: ephemeralrunnerset-viewer-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- ephemeralrunnersets
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- ephemeralrunnersets/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
@@ -6,6 +6,110 @@ metadata:
|
|||||||
creationTimestamp: null
|
creationTimestamp: null
|
||||||
name: manager-role
|
name: manager-role
|
||||||
rules:
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- autoscalinglisteners
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- autoscalinglisteners/finalizers
|
||||||
|
verbs:
|
||||||
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- autoscalinglisteners/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- autoscalingrunnersets
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- autoscalingrunnersets/finalizers
|
||||||
|
verbs:
|
||||||
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- autoscalingrunnersets/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- ephemeralrunners
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- ephemeralrunners/finalizers
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- ephemeralrunners/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- ephemeralrunnersets
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- actions.github.com
|
||||||
|
resources:
|
||||||
|
- ephemeralrunnersets/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- actions.summerwind.dev
|
- actions.summerwind.dev
|
||||||
resources:
|
resources:
|
||||||
@@ -202,6 +306,26 @@ rules:
|
|||||||
verbs:
|
verbs:
|
||||||
- create
|
- create
|
||||||
- patch
|
- patch
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- namespaces
|
||||||
|
- pods
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- namespaces/status
|
||||||
|
- pods/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- ""
|
- ""
|
||||||
resources:
|
resources:
|
||||||
@@ -249,14 +373,22 @@ rules:
|
|||||||
- patch
|
- patch
|
||||||
- update
|
- update
|
||||||
- watch
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- pods/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- ""
|
- ""
|
||||||
resources:
|
resources:
|
||||||
- secrets
|
- secrets
|
||||||
verbs:
|
verbs:
|
||||||
|
- create
|
||||||
- delete
|
- delete
|
||||||
- get
|
- get
|
||||||
- list
|
- list
|
||||||
|
- update
|
||||||
- watch
|
- watch
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- ""
|
- ""
|
||||||
@@ -266,6 +398,8 @@ rules:
|
|||||||
- create
|
- create
|
||||||
- delete
|
- delete
|
||||||
- get
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- rbac.authorization.k8s.io
|
- rbac.authorization.k8s.io
|
||||||
resources:
|
resources:
|
||||||
@@ -274,6 +408,8 @@ rules:
|
|||||||
- create
|
- create
|
||||||
- delete
|
- delete
|
||||||
- get
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- rbac.authorization.k8s.io
|
- rbac.authorization.k8s.io
|
||||||
resources:
|
resources:
|
||||||
@@ -282,3 +418,6 @@ rules:
|
|||||||
- create
|
- create
|
||||||
- delete
|
- delete
|
||||||
- get
|
- get
|
||||||
|
- list
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
|||||||
450
controllers/actions.github.com/autoscalinglistener_controller.go
Normal file
450
controllers/actions.github.com/autoscalinglistener_controller.go
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 The actions-runner-controller authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package actionsgithubcom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||||
|
|
||||||
|
v1alpha1 "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
|
||||||
|
hash "github.com/actions/actions-runner-controller/hash"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
autoscalingListenerOwnerKey = ".metadata.controller"
|
||||||
|
autoscalingListenerFinalizerName = "autoscalinglistener.actions.github.com/finalizer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AutoscalingListenerReconciler reconciles a AutoscalingListener object
|
||||||
|
type AutoscalingListenerReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Log logr.Logger
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
|
||||||
|
resourceBuilder resourceBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=core,resources=pods/status,verbs=get
|
||||||
|
// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update
|
||||||
|
// +kubebuilder:rbac:groups=core,resources=serviceaccounts,verbs=get;list;watch;create
|
||||||
|
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=create;delete;get;list;watch;update
|
||||||
|
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=create;delete;get;list;watch
|
||||||
|
// +kubebuilder:rbac:groups=actions.github.com,resources=autoscalinglisteners,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=actions.github.com,resources=autoscalinglisteners/status,verbs=get;update;patch
|
||||||
|
// +kubebuilder:rbac:groups=actions.github.com,resources=autoscalinglisteners/finalizers,verbs=update
|
||||||
|
|
||||||
|
// Reconcile a AutoscalingListener resource to meet its desired spec.
|
||||||
|
func (r *AutoscalingListenerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
|
log := r.Log.WithValues("autoscalinglistener", req.NamespacedName)
|
||||||
|
|
||||||
|
autoscalingListener := new(v1alpha1.AutoscalingListener)
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, autoscalingListener); err != nil {
|
||||||
|
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !autoscalingListener.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||||
|
if controllerutil.ContainsFinalizer(autoscalingListener, autoscalingListenerFinalizerName) {
|
||||||
|
log.Info("Deleting resources")
|
||||||
|
done, err := r.cleanupResources(ctx, autoscalingListener, log)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err, "Failed to cleanup resources after deletion")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
if !done {
|
||||||
|
log.Info("Waiting for resources to be deleted before removing finalizer")
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Removing finalizer")
|
||||||
|
err = patch(ctx, r.Client, autoscalingListener, func(obj *v1alpha1.AutoscalingListener) {
|
||||||
|
controllerutil.RemoveFinalizer(obj, autoscalingListenerFinalizerName)
|
||||||
|
})
|
||||||
|
if err != nil && !kerrors.IsNotFound(err) {
|
||||||
|
log.Error(err, "Failed to remove finalizer")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Successfully removed finalizer after cleanup")
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !controllerutil.ContainsFinalizer(autoscalingListener, autoscalingListenerFinalizerName) {
|
||||||
|
log.Info("Adding finalizer")
|
||||||
|
if err := patch(ctx, r.Client, autoscalingListener, func(obj *v1alpha1.AutoscalingListener) {
|
||||||
|
controllerutil.AddFinalizer(obj, autoscalingListenerFinalizerName)
|
||||||
|
}); err != nil {
|
||||||
|
log.Error(err, "Failed to add finalizer")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Successfully added finalizer")
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the AutoscalingRunnerSet exists
|
||||||
|
var autoscalingRunnerSet v1alpha1.AutoscalingRunnerSet
|
||||||
|
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, Name: autoscalingListener.Spec.AutoscalingRunnerSetName}, &autoscalingRunnerSet); err != nil {
|
||||||
|
log.Error(err, "Failed to find AutoscalingRunnerSet.",
|
||||||
|
"namespace", autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
|
||||||
|
"name", autoscalingListener.Spec.AutoscalingRunnerSetName)
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the GitHub config secret exists
|
||||||
|
secret := new(corev1.Secret)
|
||||||
|
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, Name: autoscalingListener.Spec.GitHubConfigSecret}, secret); err != nil {
|
||||||
|
log.Error(err, "Failed to find GitHub config secret.",
|
||||||
|
"namespace", autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
|
||||||
|
"name", autoscalingListener.Spec.GitHubConfigSecret)
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a mirror secret in the same namespace as the AutoscalingListener
|
||||||
|
mirrorSecret := new(corev1.Secret)
|
||||||
|
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Namespace, Name: scaleSetListenerSecretMirrorName(autoscalingListener)}, mirrorSecret); err != nil {
|
||||||
|
if !kerrors.IsNotFound(err) {
|
||||||
|
log.Error(err, "Unable to get listener secret mirror", "namespace", autoscalingListener.Namespace, "name", scaleSetListenerSecretMirrorName(autoscalingListener))
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a mirror secret for the listener pod in the Controller namespace for listener pod to use
|
||||||
|
log.Info("Creating a mirror listener secret for the listener pod")
|
||||||
|
return r.createSecretsForListener(ctx, autoscalingListener, secret, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure the mirror secret is up to date
|
||||||
|
mirrorSecretDataHash := mirrorSecret.Labels["secret-data-hash"]
|
||||||
|
secretDataHash := hash.ComputeTemplateHash(secret.Data)
|
||||||
|
if mirrorSecretDataHash != secretDataHash {
|
||||||
|
log.Info("Updating mirror listener secret for the listener pod", "mirrorSecretDataHash", mirrorSecretDataHash, "secretDataHash", secretDataHash)
|
||||||
|
return r.updateSecretsForListener(ctx, secret, mirrorSecret, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the runner scale set listener service account is created for the listener pod in the controller namespace
|
||||||
|
serviceAccount := new(corev1.ServiceAccount)
|
||||||
|
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Namespace, Name: scaleSetListenerServiceAccountName(autoscalingListener)}, serviceAccount); err != nil {
|
||||||
|
if !kerrors.IsNotFound(err) {
|
||||||
|
log.Error(err, "Unable to get listener service accounts", "namespace", autoscalingListener.Namespace, "name", scaleSetListenerServiceAccountName(autoscalingListener))
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a service account for the listener pod in the controller namespace
|
||||||
|
log.Info("Creating a service account for the listener pod")
|
||||||
|
return r.createServiceAccountForListener(ctx, autoscalingListener, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: make sure the service account is up to date
|
||||||
|
|
||||||
|
// Make sure the runner scale set listener role is created in the AutoscalingRunnerSet namespace
|
||||||
|
listenerRole := new(rbacv1.Role)
|
||||||
|
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, Name: scaleSetListenerRoleName(autoscalingListener)}, listenerRole); err != nil {
|
||||||
|
if !kerrors.IsNotFound(err) {
|
||||||
|
log.Error(err, "Unable to get listener role", "namespace", autoscalingListener.Spec.AutoscalingRunnerSetNamespace, "name", scaleSetListenerRoleName(autoscalingListener))
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a role for the listener pod in the AutoScalingRunnerSet namespace
|
||||||
|
log.Info("Creating a role for the listener pod")
|
||||||
|
return r.createRoleForListener(ctx, autoscalingListener, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the listener role has the up-to-date rules
|
||||||
|
existingRuleHash := listenerRole.Labels["role-policy-rules-hash"]
|
||||||
|
desiredRules := rulesForListenerRole([]string{autoscalingListener.Spec.EphemeralRunnerSetName})
|
||||||
|
desiredRulesHash := hash.ComputeTemplateHash(&desiredRules)
|
||||||
|
if existingRuleHash != desiredRulesHash {
|
||||||
|
log.Info("Updating the listener role with the up-to-date rules")
|
||||||
|
return r.updateRoleForListener(ctx, listenerRole, desiredRules, desiredRulesHash, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the runner scale set listener role binding is created
|
||||||
|
listenerRoleBinding := new(rbacv1.RoleBinding)
|
||||||
|
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, Name: scaleSetListenerRoleName(autoscalingListener)}, listenerRoleBinding); err != nil {
|
||||||
|
if !kerrors.IsNotFound(err) {
|
||||||
|
log.Error(err, "Unable to get listener role binding", "namespace", autoscalingListener.Spec.AutoscalingRunnerSetNamespace, "name", scaleSetListenerRoleName(autoscalingListener))
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a role binding for the listener pod in the AutoScalingRunnerSet namespace
|
||||||
|
log.Info("Creating a role binding for the service account and role")
|
||||||
|
return r.createRoleBindingForListener(ctx, autoscalingListener, listenerRole, serviceAccount, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: make sure the role binding has the up-to-date role and service account
|
||||||
|
|
||||||
|
listenerPod := new(corev1.Pod)
|
||||||
|
if err := r.Get(ctx, client.ObjectKey{Namespace: autoscalingListener.Namespace, Name: autoscalingListener.Name}, listenerPod); err != nil {
|
||||||
|
if !kerrors.IsNotFound(err) {
|
||||||
|
log.Error(err, "Unable to get listener pod", "namespace", autoscalingListener.Namespace, "name", autoscalingListener.Name)
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a listener pod in the controller namespace
|
||||||
|
log.Info("Creating a listener pod")
|
||||||
|
return r.createListenerPod(ctx, &autoscalingRunnerSet, autoscalingListener, serviceAccount, mirrorSecret, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The listener pod failed might mean the mirror secret is out of date
|
||||||
|
// Delete the listener pod and re-create it to make sure the mirror secret is up to date
|
||||||
|
if listenerPod.Status.Phase == corev1.PodFailed && listenerPod.DeletionTimestamp.IsZero() {
|
||||||
|
log.Info("Listener pod failed, deleting it and re-creating it", "namespace", listenerPod.Namespace, "name", listenerPod.Name, "reason", listenerPod.Status.Reason, "message", listenerPod.Status.Message)
|
||||||
|
if err := r.Delete(ctx, listenerPod); err != nil && !kerrors.IsNotFound(err) {
|
||||||
|
log.Error(err, "Unable to delete the listener pod", "namespace", listenerPod.Namespace, "name", listenerPod.Name)
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupWithManager sets up the controller with the Manager.
|
||||||
|
func (r *AutoscalingListenerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
groupVersionIndexer := func(rawObj client.Object) []string {
|
||||||
|
groupVersion := v1alpha1.GroupVersion.String()
|
||||||
|
owner := metav1.GetControllerOf(rawObj)
|
||||||
|
if owner == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...make sure it is owned by this controller
|
||||||
|
if owner.APIVersion != groupVersion || owner.Kind != "AutoscalingListener" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...and if so, return it
|
||||||
|
return []string{owner.Name}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &corev1.Pod{}, autoscalingListenerOwnerKey, groupVersionIndexer); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &corev1.ServiceAccount{}, autoscalingListenerOwnerKey, groupVersionIndexer); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
labelBasedWatchFunc := func(obj client.Object) []reconcile.Request {
|
||||||
|
var requests []reconcile.Request
|
||||||
|
labels := obj.GetLabels()
|
||||||
|
namespace, ok := labels["auto-scaling-listener-namespace"]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
name, ok := labels["auto-scaling-listener-name"]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
requests = append(requests,
|
||||||
|
reconcile.Request{
|
||||||
|
NamespacedName: types.NamespacedName{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return requests
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(&v1alpha1.AutoscalingListener{}).
|
||||||
|
Owns(&corev1.Pod{}).
|
||||||
|
Owns(&corev1.ServiceAccount{}).
|
||||||
|
Owns(&corev1.Secret{}).
|
||||||
|
Watches(&source.Kind{Type: &rbacv1.Role{}}, handler.EnqueueRequestsFromMapFunc(labelBasedWatchFunc)).
|
||||||
|
Watches(&source.Kind{Type: &rbacv1.RoleBinding{}}, handler.EnqueueRequestsFromMapFunc(labelBasedWatchFunc)).
|
||||||
|
WithEventFilter(predicate.ResourceVersionChangedPredicate{}).
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AutoscalingListenerReconciler) cleanupResources(ctx context.Context, autoscalingListener *v1alpha1.AutoscalingListener, logger logr.Logger) (done bool, err error) {
|
||||||
|
logger.Info("Cleaning up the listener pod")
|
||||||
|
listenerPod := new(corev1.Pod)
|
||||||
|
err = r.Get(ctx, types.NamespacedName{Name: autoscalingListener.Name, Namespace: autoscalingListener.Namespace}, listenerPod)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
if listenerPod.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||||
|
logger.Info("Deleting the listener pod")
|
||||||
|
if err := r.Delete(ctx, listenerPod); err != nil {
|
||||||
|
return false, fmt.Errorf("failed to delete listener pod: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
case err != nil && !kerrors.IsNotFound(err):
|
||||||
|
return false, fmt.Errorf("failed to get listener pods: %v", err)
|
||||||
|
}
|
||||||
|
logger.Info("Listener pod is deleted")
|
||||||
|
|
||||||
|
logger.Info("Cleaning up the listener service account")
|
||||||
|
listenerSa := new(corev1.ServiceAccount)
|
||||||
|
err = r.Get(ctx, types.NamespacedName{Name: scaleSetListenerServiceAccountName(autoscalingListener), Namespace: autoscalingListener.Namespace}, listenerSa)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
if listenerSa.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||||
|
logger.Info("Deleting the listener service account")
|
||||||
|
if err := r.Delete(ctx, listenerSa); err != nil {
|
||||||
|
return false, fmt.Errorf("failed to delete listener service account: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
case err != nil && !kerrors.IsNotFound(err):
|
||||||
|
return false, fmt.Errorf("failed to get listener service account: %v", err)
|
||||||
|
}
|
||||||
|
logger.Info("Listener service account is deleted")
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AutoscalingListenerReconciler) createServiceAccountForListener(ctx context.Context, autoscalingListener *v1alpha1.AutoscalingListener, logger logr.Logger) (ctrl.Result, error) {
|
||||||
|
newServiceAccount := r.resourceBuilder.newScaleSetListenerServiceAccount(autoscalingListener)
|
||||||
|
|
||||||
|
if err := ctrl.SetControllerReference(autoscalingListener, newServiceAccount, r.Scheme); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Creating listener service accounts", "namespace", newServiceAccount.Namespace, "name", newServiceAccount.Name)
|
||||||
|
if err := r.Create(ctx, newServiceAccount); err != nil {
|
||||||
|
logger.Error(err, "Unable to create listener service accounts", "namespace", newServiceAccount.Namespace, "name", newServiceAccount.Name)
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Created listener service accounts", "namespace", newServiceAccount.Namespace, "name", newServiceAccount.Name)
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AutoscalingListenerReconciler) createListenerPod(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, autoscalingListener *v1alpha1.AutoscalingListener, serviceAccount *corev1.ServiceAccount, secret *corev1.Secret, logger logr.Logger) (ctrl.Result, error) {
|
||||||
|
newPod := r.resourceBuilder.newScaleSetListenerPod(autoscalingListener, serviceAccount, secret)
|
||||||
|
|
||||||
|
if err := ctrl.SetControllerReference(autoscalingListener, newPod, r.Scheme); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Creating listener pod", "namespace", newPod.Namespace, "name", newPod.Name)
|
||||||
|
if err := r.Create(ctx, newPod); err != nil {
|
||||||
|
logger.Error(err, "Unable to create listener pod", "namespace", newPod.Namespace, "name", newPod.Name)
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Created listener pod", "namespace", newPod.Namespace, "name", newPod.Name)
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AutoscalingListenerReconciler) createSecretsForListener(ctx context.Context, autoscalingListener *v1alpha1.AutoscalingListener, secret *corev1.Secret, logger logr.Logger) (ctrl.Result, error) {
|
||||||
|
newListenerSecret := r.resourceBuilder.newScaleSetListenerSecretMirror(autoscalingListener, secret)
|
||||||
|
|
||||||
|
if err := ctrl.SetControllerReference(autoscalingListener, newListenerSecret, r.Scheme); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Creating listener secret", "namespace", newListenerSecret.Namespace, "name", newListenerSecret.Name)
|
||||||
|
if err := r.Create(ctx, newListenerSecret); err != nil {
|
||||||
|
logger.Error(err, "Unable to create listener secret", "namespace", newListenerSecret.Namespace, "name", newListenerSecret.Name)
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Created listener secret", "namespace", newListenerSecret.Namespace, "name", newListenerSecret.Name)
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AutoscalingListenerReconciler) updateSecretsForListener(ctx context.Context, secret *corev1.Secret, mirrorSecret *corev1.Secret, logger logr.Logger) (ctrl.Result, error) {
|
||||||
|
dataHash := hash.ComputeTemplateHash(secret.Data)
|
||||||
|
updatedMirrorSecret := mirrorSecret.DeepCopy()
|
||||||
|
updatedMirrorSecret.Labels["secret-data-hash"] = dataHash
|
||||||
|
updatedMirrorSecret.Data = secret.Data
|
||||||
|
|
||||||
|
logger.Info("Updating listener mirror secret", "namespace", updatedMirrorSecret.Namespace, "name", updatedMirrorSecret.Name, "hash", dataHash)
|
||||||
|
if err := r.Update(ctx, updatedMirrorSecret); err != nil {
|
||||||
|
logger.Error(err, "Unable to update listener mirror secret", "namespace", updatedMirrorSecret.Namespace, "name", updatedMirrorSecret.Name)
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Updated listener mirror secret", "namespace", updatedMirrorSecret.Namespace, "name", updatedMirrorSecret.Name, "hash", dataHash)
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AutoscalingListenerReconciler) createRoleForListener(ctx context.Context, autoscalingListener *v1alpha1.AutoscalingListener, logger logr.Logger) (ctrl.Result, error) {
|
||||||
|
newRole := r.resourceBuilder.newScaleSetListenerRole(autoscalingListener)
|
||||||
|
|
||||||
|
logger.Info("Creating listener role", "namespace", newRole.Namespace, "name", newRole.Name, "rules", newRole.Rules)
|
||||||
|
if err := r.Create(ctx, newRole); err != nil {
|
||||||
|
logger.Error(err, "Unable to create listener role", "namespace", newRole.Namespace, "name", newRole.Name, "rules", newRole.Rules)
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Created listener role", "namespace", newRole.Namespace, "name", newRole.Name, "rules", newRole.Rules)
|
||||||
|
return ctrl.Result{Requeue: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AutoscalingListenerReconciler) updateRoleForListener(ctx context.Context, listenerRole *rbacv1.Role, desiredRules []rbacv1.PolicyRule, desiredRulesHash string, logger logr.Logger) (ctrl.Result, error) {
|
||||||
|
updatedPatchRole := listenerRole.DeepCopy()
|
||||||
|
updatedPatchRole.Labels["role-policy-rules-hash"] = desiredRulesHash
|
||||||
|
updatedPatchRole.Rules = desiredRules
|
||||||
|
|
||||||
|
logger.Info("Updating listener role in namespace to have the right permission", "namespace", updatedPatchRole.Namespace, "name", updatedPatchRole.Name, "oldRules", listenerRole.Rules, "newRules", updatedPatchRole.Rules)
|
||||||
|
if err := r.Update(ctx, updatedPatchRole); err != nil {
|
||||||
|
logger.Error(err, "Unable to update listener role", "namespace", updatedPatchRole.Namespace, "name", updatedPatchRole.Name, "rules", updatedPatchRole.Rules)
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Updated listener role in namespace to have the right permission", "namespace", updatedPatchRole.Namespace, "name", updatedPatchRole.Name, "rules", updatedPatchRole.Rules)
|
||||||
|
return ctrl.Result{Requeue: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AutoscalingListenerReconciler) createRoleBindingForListener(ctx context.Context, autoscalingListener *v1alpha1.AutoscalingListener, listenerRole *rbacv1.Role, serviceAccount *corev1.ServiceAccount, logger logr.Logger) (ctrl.Result, error) {
|
||||||
|
newRoleBinding := r.resourceBuilder.newScaleSetListenerRoleBinding(autoscalingListener, listenerRole, serviceAccount)
|
||||||
|
|
||||||
|
logger.Info("Creating listener role binding",
|
||||||
|
"namespace", newRoleBinding.Namespace,
|
||||||
|
"name", newRoleBinding.Name,
|
||||||
|
"role", listenerRole.Name,
|
||||||
|
"serviceAccountNamespace", serviceAccount.Namespace,
|
||||||
|
"serviceAccount", serviceAccount.Name)
|
||||||
|
if err := r.Create(ctx, newRoleBinding); err != nil {
|
||||||
|
logger.Error(err, "Unable to create listener role binding",
|
||||||
|
"namespace", newRoleBinding.Namespace,
|
||||||
|
"name", newRoleBinding.Name,
|
||||||
|
"role", listenerRole.Name,
|
||||||
|
"serviceAccountNamespace", serviceAccount.Namespace,
|
||||||
|
"serviceAccount", serviceAccount.Name)
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Created listener role binding",
|
||||||
|
"namespace", newRoleBinding.Namespace,
|
||||||
|
"name", newRoleBinding.Name,
|
||||||
|
"role", listenerRole.Name,
|
||||||
|
"serviceAccountNamespace", serviceAccount.Namespace,
|
||||||
|
"serviceAccount", serviceAccount.Name)
|
||||||
|
return ctrl.Result{Requeue: true}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
package actionsgithubcom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
|
actionsv1alpha1 "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
autoScalingListenerTestTimeout = time.Second * 5
|
||||||
|
autoScalingListenerTestInterval = time.Millisecond * 250
|
||||||
|
autoScalingListenerTestGitHubToken = "gh_token"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Test AutoScalingListener controller", func() {
|
||||||
|
var ctx context.Context
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
autoScalingNS := new(corev1.Namespace)
|
||||||
|
autoScalingRunnerSet := new(actionsv1alpha1.AutoscalingRunnerSet)
|
||||||
|
configSecret := new(corev1.Secret)
|
||||||
|
autoScalingListener := new(actionsv1alpha1.AutoscalingListener)
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
ctx, cancel = context.WithCancel(context.TODO())
|
||||||
|
autoScalingNS = &corev1.Namespace{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "testns-autoscaling-listener" + RandStringRunes(5)},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := k8sClient.Create(ctx, autoScalingNS)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to create test namespace for AutoScalingRunnerSet")
|
||||||
|
|
||||||
|
configSecret = &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "github-config-secret",
|
||||||
|
Namespace: autoScalingNS.Name,
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"github_token": []byte(autoScalingListenerTestGitHubToken),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = k8sClient.Create(ctx, configSecret)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to create config secret")
|
||||||
|
|
||||||
|
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
|
||||||
|
Namespace: autoScalingNS.Name,
|
||||||
|
MetricsBindAddress: "0",
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to create manager")
|
||||||
|
|
||||||
|
controller := &AutoscalingListenerReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Scheme: mgr.GetScheme(),
|
||||||
|
Log: logf.Log,
|
||||||
|
}
|
||||||
|
err = controller.SetupWithManager(mgr)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
||||||
|
|
||||||
|
min := 1
|
||||||
|
max := 10
|
||||||
|
autoScalingRunnerSet = &actionsv1alpha1.AutoscalingRunnerSet{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-asrs",
|
||||||
|
Namespace: autoScalingNS.Name,
|
||||||
|
},
|
||||||
|
Spec: actionsv1alpha1.AutoscalingRunnerSetSpec{
|
||||||
|
GitHubConfigUrl: "https://github.com/owner/repo",
|
||||||
|
GitHubConfigSecret: configSecret.Name,
|
||||||
|
MaxRunners: &max,
|
||||||
|
MinRunners: &min,
|
||||||
|
Template: corev1.PodTemplateSpec{
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{
|
||||||
|
Name: "runner",
|
||||||
|
Image: "ghcr.io/actions/runner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = k8sClient.Create(ctx, autoScalingRunnerSet)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to create AutoScalingRunnerSet")
|
||||||
|
|
||||||
|
autoScalingListener = &actionsv1alpha1.AutoscalingListener{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-asl",
|
||||||
|
Namespace: autoScalingNS.Name,
|
||||||
|
},
|
||||||
|
Spec: actionsv1alpha1.AutoscalingListenerSpec{
|
||||||
|
GitHubConfigUrl: "https://github.com/owner/repo",
|
||||||
|
GitHubConfigSecret: configSecret.Name,
|
||||||
|
RunnerScaleSetId: 1,
|
||||||
|
AutoscalingRunnerSetNamespace: autoScalingRunnerSet.Namespace,
|
||||||
|
AutoscalingRunnerSetName: autoScalingRunnerSet.Name,
|
||||||
|
EphemeralRunnerSetName: "test-ers",
|
||||||
|
MaxRunners: 10,
|
||||||
|
MinRunners: 1,
|
||||||
|
Image: "ghcr.io/owner/repo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = k8sClient.Create(ctx, autoScalingListener)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to create AutoScalingListener")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
err := mgr.Start(ctx)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to start manager")
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := k8sClient.Delete(ctx, autoScalingNS)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to delete test namespace for AutoScalingRunnerSet")
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("When creating a new AutoScalingListener", func() {
|
||||||
|
It("It should create/add all required resources for a new AutoScalingListener (finalizer, secret, service account, role, rolebinding, pod)", func() {
|
||||||
|
// Check if finalizer is added
|
||||||
|
created := new(actionsv1alpha1.AutoscalingListener)
|
||||||
|
Eventually(
|
||||||
|
func() (string, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: autoScalingListener.Name, Namespace: autoScalingListener.Namespace}, created)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(created.Finalizers) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return created.Finalizers[0], nil
|
||||||
|
},
|
||||||
|
autoScalingListenerTestTimeout,
|
||||||
|
autoScalingListenerTestInterval).Should(BeEquivalentTo(autoscalingListenerFinalizerName), "AutoScalingListener should have a finalizer")
|
||||||
|
|
||||||
|
// Check if secret is created
|
||||||
|
mirrorSecret := new(corev1.Secret)
|
||||||
|
Eventually(
|
||||||
|
func() (string, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerSecretMirrorName(autoScalingListener), Namespace: autoScalingListener.Namespace}, mirrorSecret)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(mirrorSecret.Data["github_token"]), nil
|
||||||
|
},
|
||||||
|
autoScalingListenerTestTimeout,
|
||||||
|
autoScalingListenerTestInterval).Should(BeEquivalentTo(autoScalingListenerTestGitHubToken), "Mirror secret should be created")
|
||||||
|
|
||||||
|
// Check if service account is created
|
||||||
|
serviceAccount := new(corev1.ServiceAccount)
|
||||||
|
Eventually(
|
||||||
|
func() (string, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerServiceAccountName(autoScalingListener), Namespace: autoScalingListener.Namespace}, serviceAccount)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return serviceAccount.Name, nil
|
||||||
|
},
|
||||||
|
autoScalingListenerTestTimeout,
|
||||||
|
autoScalingListenerTestInterval).Should(BeEquivalentTo(scaleSetListenerServiceAccountName(autoScalingListener)), "Service account should be created")
|
||||||
|
|
||||||
|
// Check if role is created
|
||||||
|
role := new(rbacv1.Role)
|
||||||
|
Eventually(
|
||||||
|
func() ([]rbacv1.PolicyRule, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerRoleName(autoScalingListener), Namespace: autoScalingListener.Spec.AutoscalingRunnerSetNamespace}, role)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return role.Rules, nil
|
||||||
|
},
|
||||||
|
autoScalingListenerTestTimeout,
|
||||||
|
autoScalingListenerTestInterval).Should(BeEquivalentTo(rulesForListenerRole([]string{autoScalingListener.Spec.EphemeralRunnerSetName})), "Role should be created")
|
||||||
|
|
||||||
|
// Check if rolebinding is created
|
||||||
|
roleBinding := new(rbacv1.RoleBinding)
|
||||||
|
Eventually(
|
||||||
|
func() (string, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerRoleName(autoScalingListener), Namespace: autoScalingListener.Spec.AutoscalingRunnerSetNamespace}, roleBinding)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return roleBinding.RoleRef.Name, nil
|
||||||
|
},
|
||||||
|
autoScalingListenerTestTimeout,
|
||||||
|
autoScalingListenerTestInterval).Should(BeEquivalentTo(scaleSetListenerRoleName(autoScalingListener)), "Rolebinding should be created")
|
||||||
|
|
||||||
|
// Check if pod is created
|
||||||
|
pod := new(corev1.Pod)
|
||||||
|
Eventually(
|
||||||
|
func() (string, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: autoScalingListener.Name, Namespace: autoScalingListener.Namespace}, pod)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pod.Name, nil
|
||||||
|
},
|
||||||
|
autoScalingListenerTestTimeout,
|
||||||
|
autoScalingListenerTestInterval).Should(BeEquivalentTo(autoScalingListener.Name), "Pod should be created")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("When deleting a new AutoScalingListener", func() {
|
||||||
|
It("It should cleanup all resources for a deleting AutoScalingListener before removing it", func() {
|
||||||
|
// Waiting for the pod is created
|
||||||
|
pod := new(corev1.Pod)
|
||||||
|
Eventually(
|
||||||
|
func() (string, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: autoScalingListener.Name, Namespace: autoScalingListener.Namespace}, pod)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pod.Name, nil
|
||||||
|
},
|
||||||
|
autoScalingListenerTestTimeout,
|
||||||
|
autoScalingListenerTestInterval).Should(BeEquivalentTo(autoScalingListener.Name), "Pod should be created")
|
||||||
|
|
||||||
|
// Delete the AutoScalingListener
|
||||||
|
err := k8sClient.Delete(ctx, autoScalingListener)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to delete test AutoScalingListener")
|
||||||
|
|
||||||
|
// Cleanup the listener pod
|
||||||
|
Eventually(
|
||||||
|
func() error {
|
||||||
|
podList := new(corev1.PodList)
|
||||||
|
err := k8sClient.List(ctx, podList, client.InNamespace(autoScalingListener.Namespace), client.MatchingFields{autoscalingRunnerSetOwnerKey: autoScalingListener.Name})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(podList.Items) > 0 {
|
||||||
|
return fmt.Errorf("pod still exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
autoScalingListenerTestTimeout,
|
||||||
|
autoScalingListenerTestInterval).ShouldNot(Succeed(), "failed to delete pod")
|
||||||
|
|
||||||
|
// Cleanup the listener service account
|
||||||
|
Eventually(
|
||||||
|
func() error {
|
||||||
|
serviceAccountList := new(corev1.ServiceAccountList)
|
||||||
|
err := k8sClient.List(ctx, serviceAccountList, client.InNamespace(autoScalingListener.Namespace), client.MatchingFields{autoscalingRunnerSetOwnerKey: autoScalingListener.Name})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(serviceAccountList.Items) > 0 {
|
||||||
|
return fmt.Errorf("service account still exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
autoScalingListenerTestTimeout,
|
||||||
|
autoScalingListenerTestInterval).ShouldNot(Succeed(), "failed to delete service account")
|
||||||
|
|
||||||
|
// The AutoScalingListener should be deleted
|
||||||
|
Eventually(
|
||||||
|
func() error {
|
||||||
|
listenerList := new(actionsv1alpha1.AutoscalingListenerList)
|
||||||
|
err := k8sClient.List(ctx, listenerList, client.InNamespace(autoScalingListener.Namespace), client.MatchingFields{".metadata.name": autoScalingListener.Name})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(listenerList.Items) > 0 {
|
||||||
|
return fmt.Errorf("AutoScalingListener still exists")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
autoScalingListenerTestTimeout,
|
||||||
|
autoScalingListenerTestInterval).ShouldNot(Succeed(), "failed to delete AutoScalingListener")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("React to changes in the AutoScalingListener", func() {
|
||||||
|
It("It should update role to match EphemeralRunnerSet", func() {
|
||||||
|
// Waiting for the pod is created
|
||||||
|
pod := new(corev1.Pod)
|
||||||
|
Eventually(
|
||||||
|
func() (string, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: autoScalingListener.Name, Namespace: autoScalingListener.Namespace}, pod)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pod.Name, nil
|
||||||
|
},
|
||||||
|
autoScalingListenerTestTimeout,
|
||||||
|
autoScalingListenerTestInterval).Should(BeEquivalentTo(autoScalingListener.Name), "Pod should be created")
|
||||||
|
|
||||||
|
// Update the AutoScalingListener
|
||||||
|
updated := autoScalingListener.DeepCopy()
|
||||||
|
updated.Spec.EphemeralRunnerSetName = "test-ers-updated"
|
||||||
|
err := k8sClient.Patch(ctx, updated, client.MergeFrom(autoScalingListener))
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to update test AutoScalingListener")
|
||||||
|
|
||||||
|
// Check if role is updated with right rules
|
||||||
|
role := new(rbacv1.Role)
|
||||||
|
Eventually(
|
||||||
|
func() ([]rbacv1.PolicyRule, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerRoleName(autoScalingListener), Namespace: autoScalingListener.Spec.AutoscalingRunnerSetNamespace}, role)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return role.Rules, nil
|
||||||
|
},
|
||||||
|
autoScalingListenerTestTimeout,
|
||||||
|
autoScalingListenerTestInterval).Should(BeEquivalentTo(rulesForListenerRole([]string{updated.Spec.EphemeralRunnerSetName})), "Role should be updated")
|
||||||
|
})
|
||||||
|
|
||||||
|
It("It should update mirror secrets to match secret used by AutoScalingRunnerSet", func() {
|
||||||
|
// Waiting for the pod is created
|
||||||
|
pod := new(corev1.Pod)
|
||||||
|
Eventually(
|
||||||
|
func() (string, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: autoScalingListener.Name, Namespace: autoScalingListener.Namespace}, pod)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pod.Name, nil
|
||||||
|
},
|
||||||
|
autoScalingListenerTestTimeout,
|
||||||
|
autoScalingListenerTestInterval).Should(BeEquivalentTo(autoScalingListener.Name), "Pod should be created")
|
||||||
|
|
||||||
|
// Update the secret
|
||||||
|
updatedSecret := configSecret.DeepCopy()
|
||||||
|
updatedSecret.Data["github_token"] = []byte(autoScalingListenerTestGitHubToken + "_updated")
|
||||||
|
err := k8sClient.Update(ctx, updatedSecret)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to update test secret")
|
||||||
|
|
||||||
|
updatedPod := pod.DeepCopy()
|
||||||
|
updatedPod.Status.Phase = corev1.PodFailed
|
||||||
|
err = k8sClient.Status().Update(ctx, updatedPod)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to update test pod to failed")
|
||||||
|
|
||||||
|
// Check if mirror secret is updated with right data
|
||||||
|
mirrorSecret := new(corev1.Secret)
|
||||||
|
Eventually(
|
||||||
|
func() (map[string][]byte, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerSecretMirrorName(autoScalingListener), Namespace: autoScalingListener.Namespace}, mirrorSecret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mirrorSecret.Data, nil
|
||||||
|
},
|
||||||
|
autoScalingListenerTestTimeout,
|
||||||
|
autoScalingListenerTestInterval).Should(BeEquivalentTo(updatedSecret.Data), "Mirror secret should be updated")
|
||||||
|
|
||||||
|
// Check if we re-created a new pod
|
||||||
|
Eventually(
|
||||||
|
func() error {
|
||||||
|
latestPod := new(corev1.Pod)
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: autoScalingListener.Name, Namespace: autoScalingListener.Namespace}, latestPod)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if latestPod.UID == pod.UID {
|
||||||
|
return fmt.Errorf("Pod should be recreated")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
autoScalingListenerTestTimeout,
|
||||||
|
autoScalingListenerTestInterval).Should(Succeed(), "Pod should be recreated")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,506 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 The actions-runner-controller authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package actionsgithubcom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TODO: Replace with shared image.
|
||||||
|
name = "autoscaler"
|
||||||
|
autoscalingRunnerSetOwnerKey = ".metadata.controller"
|
||||||
|
LabelKeyRunnerSpecHash = "runner-spec-hash"
|
||||||
|
LabelKeyAutoScaleRunnerSetName = "auto-scale-runner-set-name"
|
||||||
|
autoscalingRunnerSetFinalizerName = "autoscalingrunnerset.actions.github.com/finalizer"
|
||||||
|
runnerScaleSetIdKey = "runner-scale-set-id"
|
||||||
|
|
||||||
|
// scaleSetListenerLabel is the key of pod.meta.labels to label
|
||||||
|
// that the pod is a listener application
|
||||||
|
scaleSetListenerLabel = "runner-scale-set-listener"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AutoscalingRunnerSetReconciler reconciles a AutoscalingRunnerSet object
|
||||||
|
type AutoscalingRunnerSetReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Log logr.Logger
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
ControllerNamespace string
|
||||||
|
DefaultRunnerScaleSetListenerImage string
|
||||||
|
DefaultRunnerScaleSetListenerImagePullSecrets []string
|
||||||
|
ActionsClient actions.MultiClient
|
||||||
|
|
||||||
|
resourceBuilder resourceBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:rbac:groups=core,resources=namespaces;pods,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=core,resources=namespaces/status;pods/status,verbs=get
|
||||||
|
// +kubebuilder:rbac:groups=actions.github.com,resources=autoscalingrunnersets,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=actions.github.com,resources=autoscalingrunnersets/status,verbs=get;update;patch
|
||||||
|
// +kubebuilder:rbac:groups=actions.github.com,resources=autoscalingrunnersets/finalizers,verbs=update
|
||||||
|
// +kubebuilder:rbac:groups=actions.github.com,resources=ephemeralrunnersets,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=actions.github.com,resources=ephemeralrunnersets/status,verbs=get;update;patch
|
||||||
|
// +kubebuilder:rbac:groups=actions.github.com,resources=autoscalinglisteners,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=actions.github.com,resources=autoscalinglisteners/status,verbs=get;update;patch
|
||||||
|
|
||||||
|
// Reconcile a AutoscalingRunnerSet resource to meet its desired spec.
|
||||||
|
func (r *AutoscalingRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
|
log := r.Log.WithValues("autoscalingrunnerset", req.NamespacedName)
|
||||||
|
|
||||||
|
autoscalingRunnerSet := new(v1alpha1.AutoscalingRunnerSet)
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, autoscalingRunnerSet); err != nil {
|
||||||
|
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !autoscalingRunnerSet.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||||
|
if controllerutil.ContainsFinalizer(autoscalingRunnerSet, autoscalingRunnerSetFinalizerName) {
|
||||||
|
log.Info("Deleting resources")
|
||||||
|
done, err := r.cleanupListener(ctx, autoscalingRunnerSet, log)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err, "Failed to clean up listener")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
if !done {
|
||||||
|
// we are going to get notified anyway to proceed with rest of the
|
||||||
|
// cleanup. No need to re-queue
|
||||||
|
log.Info("Waiting for listener to be deleted")
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
done, err = r.cleanupEphemeralRunnerSets(ctx, autoscalingRunnerSet, log)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err, "Failed to clean up ephemeral runner sets")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
if !done {
|
||||||
|
log.Info("Waiting for ephemeral runner sets to be deleted")
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Removing finalizer")
|
||||||
|
err = patch(ctx, r.Client, autoscalingRunnerSet, func(obj *v1alpha1.AutoscalingRunnerSet) {
|
||||||
|
controllerutil.RemoveFinalizer(obj, autoscalingRunnerSetFinalizerName)
|
||||||
|
})
|
||||||
|
if err != nil && !kerrors.IsNotFound(err) {
|
||||||
|
log.Error(err, "Failed to update autoscaling runner set without finalizer")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Successfully removed finalizer after cleanup")
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !controllerutil.ContainsFinalizer(autoscalingRunnerSet, autoscalingRunnerSetFinalizerName) {
|
||||||
|
log.Info("Adding finalizer")
|
||||||
|
if err := patch(ctx, r.Client, autoscalingRunnerSet, func(obj *v1alpha1.AutoscalingRunnerSet) {
|
||||||
|
controllerutil.AddFinalizer(obj, autoscalingRunnerSetFinalizerName)
|
||||||
|
}); err != nil {
|
||||||
|
log.Error(err, "Failed to update autoscaling runner set with finalizer added")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Successfully added finalizer")
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleSetIdRaw, ok := autoscalingRunnerSet.Annotations[runnerScaleSetIdKey]
|
||||||
|
if !ok {
|
||||||
|
// Need to create a new runner scale set on Actions service
|
||||||
|
log.Info("Runner scale set id annotation does not exist. Creating a new runner scale set.")
|
||||||
|
return r.createRunnerScaleSet(ctx, autoscalingRunnerSet, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
if id, err := strconv.Atoi(scaleSetIdRaw); err != nil || id <= 0 {
|
||||||
|
log.Info("Runner scale set id annotation is not an id, or is <= 0. Creating a new runner scale set.")
|
||||||
|
// something modified the scaleSetId. Try to create one
|
||||||
|
return r.createRunnerScaleSet(ctx, autoscalingRunnerSet, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
secret := new(corev1.Secret)
|
||||||
|
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingRunnerSet.Namespace, Name: autoscalingRunnerSet.Spec.GitHubConfigSecret}, secret); err != nil {
|
||||||
|
log.Error(err, "Failed to find GitHub config secret.",
|
||||||
|
"namespace", autoscalingRunnerSet.Namespace,
|
||||||
|
"name", autoscalingRunnerSet.Spec.GitHubConfigSecret)
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
existingRunnerSets, err := r.listEphemeralRunnerSets(ctx, autoscalingRunnerSet)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err, "Failed to list existing ephemeral runner sets")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
latestRunnerSet := existingRunnerSets.latest()
|
||||||
|
if latestRunnerSet == nil {
|
||||||
|
log.Info("Latest runner set does not exist. Creating a new runner set.")
|
||||||
|
return r.createEphemeralRunnerSet(ctx, autoscalingRunnerSet, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
desiredSpecHash := autoscalingRunnerSet.RunnerSetSpecHash()
|
||||||
|
for _, runnerSet := range existingRunnerSets.all() {
|
||||||
|
log.Info("Find existing ephemeral runner set", "name", runnerSet.Name, "specHash", runnerSet.Labels[LabelKeyRunnerSpecHash])
|
||||||
|
}
|
||||||
|
|
||||||
|
if desiredSpecHash != latestRunnerSet.Labels[LabelKeyRunnerSpecHash] {
|
||||||
|
log.Info("Latest runner set spec hash does not match the current autoscaling runner set. Creating a new runner set ")
|
||||||
|
return r.createEphemeralRunnerSet(ctx, autoscalingRunnerSet, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
oldRunnerSets := existingRunnerSets.old()
|
||||||
|
if len(oldRunnerSets) > 0 {
|
||||||
|
log.Info("Cleanup old ephemeral runner sets", "count", len(oldRunnerSets))
|
||||||
|
err := r.deleteEphemeralRunnerSets(ctx, oldRunnerSets, log)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err, "Failed to clean up old runner sets")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the AutoscalingListener is up and running in the controller namespace
|
||||||
|
listener := new(v1alpha1.AutoscalingListener)
|
||||||
|
if err := r.Get(ctx, client.ObjectKey{Namespace: r.ControllerNamespace, Name: scaleSetListenerName(autoscalingRunnerSet)}, listener); err != nil {
|
||||||
|
if kerrors.IsNotFound(err) {
|
||||||
|
// We don't have a listener
|
||||||
|
log.Info("Creating a new AutoscalingListener for the runner set", "ephemeralRunnerSetName", latestRunnerSet.Name)
|
||||||
|
return r.createAutoScalingListenerForRunnerSet(ctx, autoscalingRunnerSet, latestRunnerSet, log)
|
||||||
|
}
|
||||||
|
log.Error(err, "Failed to get AutoscalingListener resource")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our listener pod is out of date, so we need to delete it to get a new recreate.
|
||||||
|
if listener.Labels[LabelKeyRunnerSpecHash] != autoscalingRunnerSet.ListenerSpecHash() {
|
||||||
|
log.Info("RunnerScaleSetListener is out of date. Deleting it so that it is recreated", "name", listener.Name)
|
||||||
|
if err := r.Delete(ctx, listener); err != nil {
|
||||||
|
if kerrors.IsNotFound(err) {
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
log.Error(err, "Failed to delete AutoscalingListener resource")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Deleted RunnerScaleSetListener since existing one is out of date")
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the status of autoscaling runner set.
|
||||||
|
if latestRunnerSet.Status.CurrentReplicas != autoscalingRunnerSet.Status.CurrentRunners {
|
||||||
|
if err := patch(ctx, r.Status(), autoscalingRunnerSet, func(obj *v1alpha1.AutoscalingRunnerSet) {
|
||||||
|
obj.Status.CurrentRunners = latestRunnerSet.Status.CurrentReplicas
|
||||||
|
}); err != nil {
|
||||||
|
log.Error(err, "Failed to update autoscaling runner set status with current runner count")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AutoscalingRunnerSetReconciler) cleanupListener(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, logger logr.Logger) (done bool, err error) {
|
||||||
|
logger.Info("Cleaning up the listener")
|
||||||
|
var listener v1alpha1.AutoscalingListener
|
||||||
|
err = r.Get(ctx, client.ObjectKey{Namespace: r.ControllerNamespace, Name: scaleSetListenerName(autoscalingRunnerSet)}, &listener)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
if listener.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||||
|
logger.Info("Deleting the listener")
|
||||||
|
if err := r.Delete(ctx, &listener); err != nil {
|
||||||
|
return false, fmt.Errorf("failed to delete listener: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
case err != nil && !kerrors.IsNotFound(err):
|
||||||
|
return false, fmt.Errorf("failed to get listener: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Listener is deleted")
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AutoscalingRunnerSetReconciler) cleanupEphemeralRunnerSets(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, logger logr.Logger) (done bool, err error) {
|
||||||
|
logger.Info("Cleaning up ephemeral runner sets")
|
||||||
|
runnerSets, err := r.listEphemeralRunnerSets(ctx, autoscalingRunnerSet)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to list ephemeral runner sets: %v", err)
|
||||||
|
}
|
||||||
|
if runnerSets.empty() {
|
||||||
|
logger.Info("All ephemeral runner sets are deleted")
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Deleting all ephemeral runner sets", "count", runnerSets.count())
|
||||||
|
if err := r.deleteEphemeralRunnerSets(ctx, runnerSets.all(), logger); err != nil {
|
||||||
|
return false, fmt.Errorf("failed to delete ephemeral runner sets: %v", err)
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AutoscalingRunnerSetReconciler) deleteEphemeralRunnerSets(ctx context.Context, oldRunnerSets []v1alpha1.EphemeralRunnerSet, logger logr.Logger) error {
|
||||||
|
for i := range oldRunnerSets {
|
||||||
|
rs := &oldRunnerSets[i]
|
||||||
|
// already deleted but contains finalizer so it still exists
|
||||||
|
if !rs.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||||
|
logger.Info("Skip ephemeral runner set since it is already marked for deletion", "name", rs.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.Info("Deleting ephemeral runner set", "name", rs.Name)
|
||||||
|
if err := r.Delete(ctx, rs); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete EphemeralRunnerSet resource: %v", err)
|
||||||
|
}
|
||||||
|
logger.Info("Deleted ephemeral runner set", "name", rs.Name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AutoscalingRunnerSetReconciler) createRunnerScaleSet(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, logger logr.Logger) (ctrl.Result, error) {
|
||||||
|
logger.Info("Creating a new runner scale set")
|
||||||
|
actionsClient, err := r.actionsClientFor(ctx, autoscalingRunnerSet)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err, "Failed to initialize Actions service client for creating a new runner scale set")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
runnerScaleSet, err := actionsClient.GetRunnerScaleSet(ctx, autoscalingRunnerSet.Name)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err, "Failed to get runner scale set from Actions service")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if runnerScaleSet == nil {
|
||||||
|
runnerGroupId := 1
|
||||||
|
if len(autoscalingRunnerSet.Spec.RunnerGroup) > 0 {
|
||||||
|
runnerGroup, err := actionsClient.GetRunnerGroupByName(ctx, autoscalingRunnerSet.Spec.RunnerGroup)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err, "Failed to get runner group by name", "runnerGroup", autoscalingRunnerSet.Spec.RunnerGroup)
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
runnerGroupId = int(runnerGroup.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
runnerScaleSet, err = actionsClient.CreateRunnerScaleSet(
|
||||||
|
ctx,
|
||||||
|
&actions.RunnerScaleSet{
|
||||||
|
Name: autoscalingRunnerSet.Name,
|
||||||
|
RunnerGroupId: runnerGroupId,
|
||||||
|
Labels: []actions.Label{
|
||||||
|
{
|
||||||
|
Name: autoscalingRunnerSet.Name,
|
||||||
|
Type: "System",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RunnerSetting: actions.RunnerSetting{
|
||||||
|
Ephemeral: true,
|
||||||
|
DisableUpdate: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err, "Failed to create a new runner scale set on Actions service")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Created/Reused a runner scale set", "id", runnerScaleSet.Id)
|
||||||
|
if autoscalingRunnerSet.Annotations == nil {
|
||||||
|
autoscalingRunnerSet.Annotations = map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
autoscalingRunnerSet.Annotations[runnerScaleSetIdKey] = strconv.Itoa(runnerScaleSet.Id)
|
||||||
|
logger.Info("Adding runner scale set ID as an annotation")
|
||||||
|
if err := r.Update(ctx, autoscalingRunnerSet); err != nil {
|
||||||
|
logger.Error(err, "Failed to add runner scale set ID")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Updated with runner scale set ID as an annotation")
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AutoscalingRunnerSetReconciler) createEphemeralRunnerSet(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, log logr.Logger) (ctrl.Result, error) {
|
||||||
|
desiredRunnerSet, err := r.resourceBuilder.newEphemeralRunnerSet(autoscalingRunnerSet)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err, "Could not create EphemeralRunnerSet")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ctrl.SetControllerReference(autoscalingRunnerSet, desiredRunnerSet, r.Scheme); err != nil {
|
||||||
|
log.Error(err, "Failed to set controller reference to a new EphemeralRunnerSet")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Creating a new EphemeralRunnerSet resource", "name", desiredRunnerSet.Name)
|
||||||
|
if err := r.Create(ctx, desiredRunnerSet); err != nil {
|
||||||
|
log.Error(err, "Failed to create EphemeralRunnerSet resource")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Created a new EphemeralRunnerSet resource", "name", desiredRunnerSet.Name)
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AutoscalingRunnerSetReconciler) createAutoScalingListenerForRunnerSet(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet, log logr.Logger) (ctrl.Result, error) {
|
||||||
|
var imagePullSecrets []corev1.LocalObjectReference
|
||||||
|
for _, imagePullSecret := range r.DefaultRunnerScaleSetListenerImagePullSecrets {
|
||||||
|
imagePullSecrets = append(imagePullSecrets, corev1.LocalObjectReference{
|
||||||
|
Name: imagePullSecret,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
autoscalingListener, err := r.resourceBuilder.newAutoScalingListener(autoscalingRunnerSet, ephemeralRunnerSet, r.ControllerNamespace, r.DefaultRunnerScaleSetListenerImage, imagePullSecrets)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err, "Could not create AutoscalingListener spec")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Creating a new AutoscalingListener resource", "name", autoscalingListener.Name, "namespace", autoscalingListener.Namespace)
|
||||||
|
if err := r.Create(ctx, autoscalingListener); err != nil {
|
||||||
|
log.Error(err, "Failed to create AutoscalingListener resource")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Created a new AutoscalingListener resource", "name", autoscalingListener.Name, "namespace", autoscalingListener.Namespace)
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AutoscalingRunnerSetReconciler) listEphemeralRunnerSets(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet) (*EphemeralRunnerSets, error) {
|
||||||
|
list := new(v1alpha1.EphemeralRunnerSetList)
|
||||||
|
if err := r.List(ctx, list, client.InNamespace(autoscalingRunnerSet.Namespace), client.MatchingFields{autoscalingRunnerSetOwnerKey: autoscalingRunnerSet.Name}); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list ephemeral runner sets: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &EphemeralRunnerSets{list: list}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AutoscalingRunnerSetReconciler) actionsClientFor(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet) (actions.ActionsService, error) {
|
||||||
|
var configSecret corev1.Secret
|
||||||
|
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingRunnerSet.Namespace, Name: autoscalingRunnerSet.Spec.GitHubConfigSecret}, &configSecret); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find GitHub config secret: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.ActionsClient.GetClientFromSecret(ctx, autoscalingRunnerSet.Spec.GitHubConfigUrl, autoscalingRunnerSet.Namespace, configSecret.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupWithManager sets up the controller with the Manager.
|
||||||
|
func (r *AutoscalingRunnerSetReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
groupVersionIndexer := func(rawObj client.Object) []string {
|
||||||
|
groupVersion := v1alpha1.GroupVersion.String()
|
||||||
|
owner := metav1.GetControllerOf(rawObj)
|
||||||
|
if owner == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...make sure it is owned by this controller
|
||||||
|
if owner.APIVersion != groupVersion || owner.Kind != "AutoscalingRunnerSet" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...and if so, return it
|
||||||
|
return []string{owner.Name}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &v1alpha1.EphemeralRunnerSet{}, autoscalingRunnerSetOwnerKey, groupVersionIndexer); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(&v1alpha1.AutoscalingRunnerSet{}).
|
||||||
|
Owns(&v1alpha1.EphemeralRunnerSet{}).
|
||||||
|
Watches(&source.Kind{Type: &v1alpha1.AutoscalingListener{}}, handler.EnqueueRequestsFromMapFunc(
|
||||||
|
func(o client.Object) []reconcile.Request {
|
||||||
|
autoscalingListener := o.(*v1alpha1.AutoscalingListener)
|
||||||
|
return []reconcile.Request{
|
||||||
|
{
|
||||||
|
NamespacedName: types.NamespacedName{
|
||||||
|
Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
|
||||||
|
Name: autoscalingListener.Spec.AutoscalingRunnerSetName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)).
|
||||||
|
WithEventFilter(predicate.ResourceVersionChangedPredicate{}).
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: if this is logic should be used for other resources,
|
||||||
|
// consider using generics
|
||||||
|
type EphemeralRunnerSets struct {
|
||||||
|
list *v1alpha1.EphemeralRunnerSetList
|
||||||
|
sorted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *EphemeralRunnerSets) latest() *v1alpha1.EphemeralRunnerSet {
|
||||||
|
if rs.empty() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !rs.sorted {
|
||||||
|
rs.sort()
|
||||||
|
}
|
||||||
|
return rs.list.Items[0].DeepCopy()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *EphemeralRunnerSets) old() []v1alpha1.EphemeralRunnerSet {
|
||||||
|
if rs.empty() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !rs.sorted {
|
||||||
|
rs.sort()
|
||||||
|
}
|
||||||
|
copy := rs.list.DeepCopy()
|
||||||
|
return copy.Items[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *EphemeralRunnerSets) all() []v1alpha1.EphemeralRunnerSet {
|
||||||
|
if rs.empty() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
copy := rs.list.DeepCopy()
|
||||||
|
return copy.Items
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *EphemeralRunnerSets) empty() bool {
|
||||||
|
return rs.list == nil || len(rs.list.Items) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *EphemeralRunnerSets) sort() {
|
||||||
|
sort.Slice(rs.list.Items, func(i, j int) bool {
|
||||||
|
return rs.list.Items[i].GetCreationTimestamp().After(rs.list.Items[j].GetCreationTimestamp().Time)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *EphemeralRunnerSets) count() int {
|
||||||
|
return len(rs.list.Items)
|
||||||
|
}
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
package actionsgithubcom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
|
actionsv1alpha1 "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions/fake"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
autoScalingRunnerSetTestTimeout = time.Second * 5
|
||||||
|
autoScalingRunnerSetTestInterval = time.Millisecond * 250
|
||||||
|
autoScalingRunnerSetTestGitHubToken = "gh_token"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Test AutoScalingRunnerSet controller", func() {
|
||||||
|
var ctx context.Context
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
autoScalingNS := new(corev1.Namespace)
|
||||||
|
autoScalingRunnerSet := new(actionsv1alpha1.AutoscalingRunnerSet)
|
||||||
|
configSecret := new(corev1.Secret)
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
ctx, cancel = context.WithCancel(context.TODO())
|
||||||
|
autoScalingNS = &corev1.Namespace{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "testns-autoscaling" + RandStringRunes(5)},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := k8sClient.Create(ctx, autoScalingNS)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to create test namespace for AutoScalingRunnerSet")
|
||||||
|
|
||||||
|
configSecret = &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "github-config-secret",
|
||||||
|
Namespace: autoScalingNS.Name,
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"github_token": []byte(autoScalingRunnerSetTestGitHubToken),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = k8sClient.Create(ctx, configSecret)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to create config secret")
|
||||||
|
|
||||||
|
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
|
||||||
|
Namespace: autoScalingNS.Name,
|
||||||
|
MetricsBindAddress: "0",
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to create manager")
|
||||||
|
|
||||||
|
controller := &AutoscalingRunnerSetReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Scheme: mgr.GetScheme(),
|
||||||
|
Log: logf.Log,
|
||||||
|
ControllerNamespace: autoScalingNS.Name,
|
||||||
|
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
|
||||||
|
ActionsClient: fake.NewMultiClient(),
|
||||||
|
}
|
||||||
|
err = controller.SetupWithManager(mgr)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
||||||
|
|
||||||
|
min := 1
|
||||||
|
max := 10
|
||||||
|
autoScalingRunnerSet = &actionsv1alpha1.AutoscalingRunnerSet{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-asrs",
|
||||||
|
Namespace: autoScalingNS.Name,
|
||||||
|
},
|
||||||
|
Spec: actionsv1alpha1.AutoscalingRunnerSetSpec{
|
||||||
|
GitHubConfigUrl: "https://github.com/owner/repo",
|
||||||
|
GitHubConfigSecret: configSecret.Name,
|
||||||
|
MaxRunners: &max,
|
||||||
|
MinRunners: &min,
|
||||||
|
Template: corev1.PodTemplateSpec{
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{
|
||||||
|
Name: "runner",
|
||||||
|
Image: "ghcr.io/actions/runner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = k8sClient.Create(ctx, autoScalingRunnerSet)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to create AutoScalingRunnerSet")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
err := mgr.Start(ctx)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to start manager")
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := k8sClient.Delete(ctx, autoScalingNS)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to delete test namespace for AutoScalingRunnerSet")
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("When creating a new AutoScalingRunnerSet", func() {
|
||||||
|
It("It should create/add all required resources for a new AutoScalingRunnerSet (finalizer, runnerscaleset, ephemeralrunnerset, listener)", func() {
|
||||||
|
// Check if finalizer is added
|
||||||
|
created := new(actionsv1alpha1.AutoscalingRunnerSet)
|
||||||
|
Eventually(
|
||||||
|
func() (string, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: autoScalingRunnerSet.Name, Namespace: autoScalingRunnerSet.Namespace}, created)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(created.Finalizers) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return created.Finalizers[0], nil
|
||||||
|
},
|
||||||
|
autoScalingRunnerSetTestTimeout,
|
||||||
|
autoScalingRunnerSetTestInterval).Should(BeEquivalentTo(autoscalingRunnerSetFinalizerName), "AutoScalingRunnerSet should have a finalizer")
|
||||||
|
|
||||||
|
// Check if runner scale set is created on service
|
||||||
|
Eventually(
|
||||||
|
func() (string, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: autoScalingRunnerSet.Name, Namespace: autoScalingRunnerSet.Namespace}, created)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := created.Annotations[runnerScaleSetIdKey]; !ok {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return created.Annotations[runnerScaleSetIdKey], nil
|
||||||
|
},
|
||||||
|
autoScalingRunnerSetTestTimeout,
|
||||||
|
autoScalingRunnerSetTestInterval).Should(BeEquivalentTo("1"), "RunnerScaleSet should be created/fetched and update the AutoScalingRunnerSet's annotation")
|
||||||
|
|
||||||
|
// Check if ephemeral runner set is created
|
||||||
|
Eventually(
|
||||||
|
func() (int, error) {
|
||||||
|
runnerSetList := new(actionsv1alpha1.EphemeralRunnerSetList)
|
||||||
|
err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoScalingRunnerSet.Namespace))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(runnerSetList.Items), nil
|
||||||
|
},
|
||||||
|
autoScalingRunnerSetTestTimeout,
|
||||||
|
autoScalingRunnerSetTestInterval).Should(BeEquivalentTo(1), "Only one EphemeralRunnerSet should be created")
|
||||||
|
|
||||||
|
// Check if listener is created
|
||||||
|
Eventually(
|
||||||
|
func() error {
|
||||||
|
return k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoScalingRunnerSet), Namespace: autoScalingRunnerSet.Namespace}, new(actionsv1alpha1.AutoscalingListener))
|
||||||
|
},
|
||||||
|
autoScalingRunnerSetTestTimeout,
|
||||||
|
autoScalingRunnerSetTestInterval).Should(Succeed(), "Listener should be created")
|
||||||
|
|
||||||
|
// Check if status is updated
|
||||||
|
runnerSetList := new(actionsv1alpha1.EphemeralRunnerSetList)
|
||||||
|
err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoScalingRunnerSet.Namespace))
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to list EphemeralRunnerSet")
|
||||||
|
Expect(len(runnerSetList.Items)).To(BeEquivalentTo(1), "Only one EphemeralRunnerSet should be created")
|
||||||
|
runnerSet := runnerSetList.Items[0]
|
||||||
|
statusUpdate := runnerSet.DeepCopy()
|
||||||
|
statusUpdate.Status.CurrentReplicas = 100
|
||||||
|
err = k8sClient.Status().Patch(ctx, statusUpdate, client.MergeFrom(&runnerSet))
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to patch EphemeralRunnerSet status")
|
||||||
|
|
||||||
|
Eventually(
|
||||||
|
func() (int, error) {
|
||||||
|
updated := new(actionsv1alpha1.AutoscalingRunnerSet)
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: autoScalingRunnerSet.Name, Namespace: autoScalingRunnerSet.Namespace}, updated)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to get AutoScalingRunnerSet: %w", err)
|
||||||
|
}
|
||||||
|
return updated.Status.CurrentRunners, nil
|
||||||
|
},
|
||||||
|
autoScalingRunnerSetTestTimeout,
|
||||||
|
autoScalingRunnerSetTestInterval).Should(BeEquivalentTo(100), "AutoScalingRunnerSet status should be updated")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("When deleting a new AutoScalingRunnerSet", func() {
|
||||||
|
It("It should cleanup all resources for a deleting AutoScalingRunnerSet before removing it", func() {
|
||||||
|
// Wait till the listener is created
|
||||||
|
Eventually(
|
||||||
|
func() error {
|
||||||
|
return k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoScalingRunnerSet), Namespace: autoScalingRunnerSet.Namespace}, new(actionsv1alpha1.AutoscalingListener))
|
||||||
|
},
|
||||||
|
autoScalingRunnerSetTestTimeout,
|
||||||
|
autoScalingRunnerSetTestInterval).Should(Succeed(), "Listener should be created")
|
||||||
|
|
||||||
|
// Delete the AutoScalingRunnerSet
|
||||||
|
err := k8sClient.Delete(ctx, autoScalingRunnerSet)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to delete AutoScalingRunnerSet")
|
||||||
|
|
||||||
|
// Check if the listener is deleted
|
||||||
|
Eventually(
|
||||||
|
func() error {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoScalingRunnerSet), Namespace: autoScalingRunnerSet.Namespace}, new(actionsv1alpha1.AutoscalingListener))
|
||||||
|
if err != nil && errors.IsNotFound(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("listener is not deleted")
|
||||||
|
},
|
||||||
|
autoScalingRunnerSetTestTimeout,
|
||||||
|
autoScalingRunnerSetTestInterval).Should(Succeed(), "Listener should be deleted")
|
||||||
|
|
||||||
|
// Check if all the EphemeralRunnerSet is deleted
|
||||||
|
Eventually(
|
||||||
|
func() error {
|
||||||
|
runnerSetList := new(actionsv1alpha1.EphemeralRunnerSetList)
|
||||||
|
err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoScalingRunnerSet.Namespace))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runnerSetList.Items) != 0 {
|
||||||
|
return fmt.Errorf("EphemeralRunnerSet is not deleted, count=%v", len(runnerSetList.Items))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
autoScalingRunnerSetTestTimeout,
|
||||||
|
autoScalingRunnerSetTestInterval).Should(Succeed(), "All EphemeralRunnerSet should be deleted")
|
||||||
|
|
||||||
|
// Check if the AutoScalingRunnerSet is deleted
|
||||||
|
Eventually(
|
||||||
|
func() error {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: autoScalingRunnerSet.Name, Namespace: autoScalingRunnerSet.Namespace}, new(actionsv1alpha1.AutoscalingRunnerSet))
|
||||||
|
if err != nil && errors.IsNotFound(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("AutoScalingRunnerSet is not deleted")
|
||||||
|
},
|
||||||
|
autoScalingRunnerSetTestTimeout,
|
||||||
|
autoScalingRunnerSetTestInterval).Should(Succeed(), "AutoScalingRunnerSet should be deleted")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("When updating a new AutoScalingRunnerSet", func() {
|
||||||
|
It("It should re-create EphemeralRunnerSet and Listener as needed when updating AutoScalingRunnerSet", func() {
|
||||||
|
// Wait till the listener is created
|
||||||
|
listener := new(actionsv1alpha1.AutoscalingListener)
|
||||||
|
Eventually(
|
||||||
|
func() error {
|
||||||
|
return k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoScalingRunnerSet), Namespace: autoScalingRunnerSet.Namespace}, listener)
|
||||||
|
},
|
||||||
|
autoScalingRunnerSetTestTimeout,
|
||||||
|
autoScalingRunnerSetTestInterval).Should(Succeed(), "Listener should be created")
|
||||||
|
|
||||||
|
runnerSetList := new(actionsv1alpha1.EphemeralRunnerSetList)
|
||||||
|
err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoScalingRunnerSet.Namespace))
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to list EphemeralRunnerSet")
|
||||||
|
Expect(len(runnerSetList.Items)).To(Equal(1), "There should be 1 EphemeralRunnerSet")
|
||||||
|
runnerSet := runnerSetList.Items[0]
|
||||||
|
|
||||||
|
// Update the AutoScalingRunnerSet.Spec.Template
|
||||||
|
// This should trigger re-creation of EphemeralRunnerSet and Listener
|
||||||
|
patched := autoScalingRunnerSet.DeepCopy()
|
||||||
|
patched.Spec.Template.Spec.PriorityClassName = "test-priority-class"
|
||||||
|
err = k8sClient.Patch(ctx, patched, client.MergeFrom(autoScalingRunnerSet))
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to patch AutoScalingRunnerSet")
|
||||||
|
autoScalingRunnerSet = patched.DeepCopy()
|
||||||
|
|
||||||
|
// We should create a new EphemeralRunnerSet and delete the old one, eventually, we will have only one EphemeralRunnerSet
|
||||||
|
Eventually(
|
||||||
|
func() (string, error) {
|
||||||
|
runnerSetList := new(actionsv1alpha1.EphemeralRunnerSetList)
|
||||||
|
err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoScalingRunnerSet.Namespace))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runnerSetList.Items) != 1 {
|
||||||
|
return "", fmt.Errorf("We should have only 1 EphemeralRunnerSet, but got %v", len(runnerSetList.Items))
|
||||||
|
}
|
||||||
|
|
||||||
|
return runnerSetList.Items[0].Labels[LabelKeyRunnerSpecHash], nil
|
||||||
|
},
|
||||||
|
autoScalingRunnerSetTestTimeout,
|
||||||
|
autoScalingRunnerSetTestInterval).ShouldNot(BeEquivalentTo(runnerSet.Labels[LabelKeyRunnerSpecHash]), "New EphemeralRunnerSet should be created")
|
||||||
|
|
||||||
|
// We should create a new listener
|
||||||
|
Eventually(
|
||||||
|
func() (string, error) {
|
||||||
|
listener := new(actionsv1alpha1.AutoscalingListener)
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoScalingRunnerSet), Namespace: autoScalingRunnerSet.Namespace}, listener)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return listener.Spec.EphemeralRunnerSetName, nil
|
||||||
|
},
|
||||||
|
autoScalingRunnerSetTestTimeout,
|
||||||
|
autoScalingRunnerSetTestInterval).ShouldNot(BeEquivalentTo(runnerSet.Name), "New Listener should be created")
|
||||||
|
|
||||||
|
// Only update the Spec for the AutoScalingListener
|
||||||
|
// This should trigger re-creation of the Listener only
|
||||||
|
runnerSetList = new(actionsv1alpha1.EphemeralRunnerSetList)
|
||||||
|
err = k8sClient.List(ctx, runnerSetList, client.InNamespace(autoScalingRunnerSet.Namespace))
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to list EphemeralRunnerSet")
|
||||||
|
Expect(len(runnerSetList.Items)).To(Equal(1), "There should be 1 EphemeralRunnerSet")
|
||||||
|
runnerSet = runnerSetList.Items[0]
|
||||||
|
|
||||||
|
listener = new(actionsv1alpha1.AutoscalingListener)
|
||||||
|
err = k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoScalingRunnerSet), Namespace: autoScalingRunnerSet.Namespace}, listener)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to get Listener")
|
||||||
|
|
||||||
|
patched = autoScalingRunnerSet.DeepCopy()
|
||||||
|
min := 10
|
||||||
|
patched.Spec.MinRunners = &min
|
||||||
|
err = k8sClient.Patch(ctx, patched, client.MergeFrom(autoScalingRunnerSet))
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to patch AutoScalingRunnerSet")
|
||||||
|
|
||||||
|
// We should not re-create a new EphemeralRunnerSet
|
||||||
|
Consistently(
|
||||||
|
func() (string, error) {
|
||||||
|
runnerSetList := new(actionsv1alpha1.EphemeralRunnerSetList)
|
||||||
|
err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoScalingRunnerSet.Namespace))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runnerSetList.Items) != 1 {
|
||||||
|
return "", fmt.Errorf("We should have only 1 EphemeralRunnerSet, but got %v", len(runnerSetList.Items))
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(runnerSetList.Items[0].UID), nil
|
||||||
|
},
|
||||||
|
autoScalingRunnerSetTestTimeout,
|
||||||
|
autoScalingRunnerSetTestInterval).Should(BeEquivalentTo(string(runnerSet.UID)), "New EphemeralRunnerSet should not be created")
|
||||||
|
|
||||||
|
// We should only re-create a new listener
|
||||||
|
Eventually(
|
||||||
|
func() (string, error) {
|
||||||
|
listener := new(actionsv1alpha1.AutoscalingListener)
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoScalingRunnerSet), Namespace: autoScalingRunnerSet.Namespace}, listener)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(listener.UID), nil
|
||||||
|
},
|
||||||
|
autoScalingRunnerSetTestTimeout,
|
||||||
|
autoScalingRunnerSetTestInterval).ShouldNot(BeEquivalentTo(string(listener.UID)), "New Listener should be created")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
22
controllers/actions.github.com/clientutil.go
Normal file
22
controllers/actions.github.com/clientutil.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package actionsgithubcom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
kclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type object[T kclient.Object] interface {
|
||||||
|
kclient.Object
|
||||||
|
DeepCopy() T
|
||||||
|
}
|
||||||
|
|
||||||
|
type patcher interface {
|
||||||
|
Patch(ctx context.Context, obj kclient.Object, patch kclient.Patch, opts ...kclient.PatchOption) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func patch[T object[T]](ctx context.Context, client patcher, obj T, update func(obj T)) error {
|
||||||
|
original := obj.DeepCopy()
|
||||||
|
update(obj)
|
||||||
|
return client.Patch(ctx, obj, kclient.MergeFrom(original))
|
||||||
|
}
|
||||||
10
controllers/actions.github.com/constants.go
Normal file
10
controllers/actions.github.com/constants.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package actionsgithubcom
|
||||||
|
|
||||||
|
const (
|
||||||
|
LabelKeyRunnerTemplateHash = "runner-template-hash"
|
||||||
|
LabelKeyPodTemplateHash = "pod-template-hash"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
EnvVarRunnerJITConfig = "ACTIONS_RUNNER_INPUT_JITCONFIG"
|
||||||
|
)
|
||||||
645
controllers/actions.github.com/ephemeralrunner_controller.go
Normal file
645
controllers/actions.github.com/ephemeralrunner_controller.go
Normal file
@@ -0,0 +1,645 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 The actions-runner-controller authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package actionsgithubcom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
"go.uber.org/multierr"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// EphemeralRunnerContainerName is the name of the runner container.
|
||||||
|
// It represents the name of the container running the self-hosted runner image.
|
||||||
|
EphemeralRunnerContainerName = "runner"
|
||||||
|
|
||||||
|
ephemeralRunnerFinalizerName = "ephemeralrunner.actions.github.com/finalizer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EphemeralRunnerReconciler reconciles a EphemeralRunner object
|
||||||
|
type EphemeralRunnerReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Log logr.Logger
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
ActionsClient actions.MultiClient
|
||||||
|
resourceBuilder resourceBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:rbac:groups=actions.github.com,resources=ephemeralrunners,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=actions.github.com,resources=ephemeralrunners/status,verbs=get;update;patch
|
||||||
|
// +kubebuilder:rbac:groups=actions.github.com,resources=ephemeralrunners/finalizers,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;delete
|
||||||
|
// +kubebuilder:rbac:groups=core,resources=pods/finalizers,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
|
||||||
|
// +kubebuilder:rbac:groups=core,resources=serviceaccounts,verbs=create;delete;get
|
||||||
|
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=create;delete;get
|
||||||
|
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=create;delete;get
|
||||||
|
|
||||||
|
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||||
|
// move the current state of the cluster closer to the desired state.
|
||||||
|
//
|
||||||
|
// For more details, check Reconcile and its Result here:
|
||||||
|
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.6.4/pkg/reconcile
|
||||||
|
func (r *EphemeralRunnerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
|
log := r.Log.WithValues("ephemeralrunner", req.NamespacedName)
|
||||||
|
|
||||||
|
ephemeralRunner := new(v1alpha1.EphemeralRunner)
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, ephemeralRunner); err != nil {
|
||||||
|
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ephemeralRunner.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||||
|
if controllerutil.ContainsFinalizer(ephemeralRunner, ephemeralRunnerFinalizerName) {
|
||||||
|
log.Info("Finalizing ephemeral runner")
|
||||||
|
done, err := r.cleanupResources(ctx, ephemeralRunner, log)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err, "Failed to clean up ephemeral runner owned resources")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
if !done {
|
||||||
|
log.Info("Waiting for ephemeral runner owned resources to be deleted")
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
done, err = r.cleanupContainerHooksResources(ctx, ephemeralRunner, log)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err, "Failed to clean up container hooks resources")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
if !done {
|
||||||
|
log.Info("Waiting for container hooks resources to be deleted")
|
||||||
|
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Removing finalizer")
|
||||||
|
err = patch(ctx, r.Client, ephemeralRunner, func(obj *v1alpha1.EphemeralRunner) {
|
||||||
|
controllerutil.RemoveFinalizer(obj, ephemeralRunnerFinalizerName)
|
||||||
|
})
|
||||||
|
if err != nil && !kerrors.IsNotFound(err) {
|
||||||
|
log.Error(err, "Failed to update ephemeral runner without the finalizer")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Successfully removed finalizer after cleanup")
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !controllerutil.ContainsFinalizer(ephemeralRunner, ephemeralRunnerFinalizerName) {
|
||||||
|
log.Info("Adding finalizer")
|
||||||
|
if err := patch(ctx, r.Client, ephemeralRunner, func(obj *v1alpha1.EphemeralRunner) {
|
||||||
|
controllerutil.AddFinalizer(obj, ephemeralRunnerFinalizerName)
|
||||||
|
}); err != nil {
|
||||||
|
log.Error(err, "Failed to update with finalizer set")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Successfully added finalizer")
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ephemeralRunner.Status.Phase == corev1.PodSucceeded || ephemeralRunner.Status.Phase == corev1.PodFailed {
|
||||||
|
// Stop reconciling on this object.
|
||||||
|
// The EphemeralRunnerSet is responsible for cleaning it up.
|
||||||
|
log.Info("EphemeralRunner has already finished. Stopping reconciliation and waiting for EphemeralRunnerSet to clean it up", "phase", ephemeralRunner.Status.Phase)
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ephemeralRunner.Status.RunnerId == 0 {
|
||||||
|
log.Info("Creating new ephemeral runner registration and updating status with runner config")
|
||||||
|
return r.updateStatusWithRunnerConfig(ctx, ephemeralRunner, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
secret := new(corev1.Secret)
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, secret); err != nil {
|
||||||
|
if !kerrors.IsNotFound(err) {
|
||||||
|
log.Error(err, "Failed to fetch secret")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
// create secret if not created
|
||||||
|
log.Info("Creating new ephemeral runner secret for jitconfig.")
|
||||||
|
return r.createSecret(ctx, ephemeralRunner, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
pod := new(corev1.Pod)
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, pod); err != nil {
|
||||||
|
switch {
|
||||||
|
case !kerrors.IsNotFound(err):
|
||||||
|
log.Error(err, "Failed to fetch the pod")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
|
||||||
|
case len(ephemeralRunner.Status.Failures) > 5:
|
||||||
|
log.Info("EphemeralRunner has failed more than 5 times. Marking it as failed")
|
||||||
|
if err := r.markAsFailed(ctx, ephemeralRunner, log); err != nil {
|
||||||
|
log.Error(err, "Failed to set ephemeral runner to phase Failed")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Pod was not found. Create if the pod has never been created
|
||||||
|
log.Info("Creating new EphemeralRunner pod.")
|
||||||
|
return r.createPod(ctx, ephemeralRunner, secret, log)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cs := runnerContainerStatus(pod)
|
||||||
|
switch {
|
||||||
|
case cs == nil:
|
||||||
|
// starting, no container state yet
|
||||||
|
log.Info("Waiting for runner container status to be available")
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
case cs.State.Terminated == nil: // still running or evicted
|
||||||
|
if pod.Status.Phase == corev1.PodFailed && pod.Status.Reason == "Evicted" {
|
||||||
|
log.Info("Pod set the termination phase, but container state is not terminated. Deleting pod",
|
||||||
|
"PodPhase", pod.Status.Phase,
|
||||||
|
"PodReason", pod.Status.Reason,
|
||||||
|
"PodMessage", pod.Status.Message,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := r.deletePodAsFailed(ctx, ephemeralRunner, pod, log); err != nil {
|
||||||
|
log.Error(err, "failed to delete pod as failed on pod.Status.Phase: Failed")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Ephemeral runner container is still running")
|
||||||
|
if err := r.updateRunStatusFromPod(ctx, ephemeralRunner, pod, log); err != nil {
|
||||||
|
log.Info("Failed to update ephemeral runner status. Requeue to not miss this event")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
|
||||||
|
case cs.State.Terminated.ExitCode != 0: // failed
|
||||||
|
log.Info("Ephemeral runner container failed", "exitCode", cs.State.Terminated.ExitCode)
|
||||||
|
if err := r.deletePodAsFailed(ctx, ephemeralRunner, pod, log); err != nil {
|
||||||
|
log.Error(err, "Failed to delete runner pod on failure")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
// pod succeeded. We double-check with the service if the runner exists.
|
||||||
|
// The reason is that image can potentially finish with status 0, but not pick up the job.
|
||||||
|
existsInService, err := r.runnerRegisteredWithService(ctx, ephemeralRunner.DeepCopy(), log)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err, "Failed to check if runner is registered with the service")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
if !existsInService {
|
||||||
|
// the runner does not exist in the service, so it must be done
|
||||||
|
log.Info("Ephemeral runner has finished since it does not exist in the service anymore")
|
||||||
|
if err := r.markAsFinished(ctx, ephemeralRunner, log); err != nil {
|
||||||
|
log.Error(err, "Failed to mark ephemeral runner as finished")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The runner still exists. This can happen if the pod exited with 0 but fails to start
|
||||||
|
log.Info("Ephemeral runner pod has finished, but the runner still exists in the service. Deleting the pod to restart it.")
|
||||||
|
if err := r.deletePodAsFailed(ctx, ephemeralRunner, pod, log); err != nil {
|
||||||
|
log.Error(err, "failed to delete a pod that still exists in the service")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EphemeralRunnerReconciler) cleanupResources(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) (deleted bool, err error) {
|
||||||
|
log.Info("Cleaning up the runner pod")
|
||||||
|
pod := new(corev1.Pod)
|
||||||
|
err = r.Get(ctx, types.NamespacedName{Namespace: ephemeralRunner.Namespace, Name: ephemeralRunner.Name}, pod)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
if pod.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||||
|
log.Info("Deleting the runner pod")
|
||||||
|
if err := r.Delete(ctx, pod); err != nil && !kerrors.IsNotFound(err) {
|
||||||
|
return false, fmt.Errorf("failed to delete pod: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
case err != nil && !kerrors.IsNotFound(err):
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
log.Info("Pod is deleted")
|
||||||
|
|
||||||
|
log.Info("Cleaning up the runner jitconfig secret")
|
||||||
|
secret := new(corev1.Secret)
|
||||||
|
err = r.Get(ctx, types.NamespacedName{Namespace: ephemeralRunner.Namespace, Name: ephemeralRunner.Name}, secret)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
if secret.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||||
|
log.Info("Deleting the jitconfig secret")
|
||||||
|
if err := r.Delete(ctx, secret); err != nil && !kerrors.IsNotFound(err) {
|
||||||
|
return false, fmt.Errorf("failed to delete secret: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
case err != nil && !kerrors.IsNotFound(err):
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
log.Info("Secret is deleted")
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EphemeralRunnerReconciler) cleanupContainerHooksResources(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) (done bool, err error) {
|
||||||
|
log.Info("Cleaning up runner linked pods")
|
||||||
|
done, err = r.cleanupRunnerLinkedPods(ctx, ephemeralRunner, log)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to clean up runner linked pods: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !done {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Cleaning up runner linked secrets")
|
||||||
|
done, err = r.cleanupRunnerLinkedSecrets(ctx, ephemeralRunner, log)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return done, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EphemeralRunnerReconciler) cleanupRunnerLinkedPods(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) (done bool, err error) {
|
||||||
|
runnerLinedLabels := client.MatchingLabels(
|
||||||
|
map[string]string{
|
||||||
|
"runner-pod": ephemeralRunner.Name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
var runnerLinkedPodList corev1.PodList
|
||||||
|
err = r.List(ctx, &runnerLinkedPodList, client.InNamespace(ephemeralRunner.Namespace), runnerLinedLabels)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to list runner-linked pods: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runnerLinkedPodList.Items) == 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Deleting container hooks runner-linked pods", "count", len(runnerLinkedPodList.Items))
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
for i := range runnerLinkedPodList.Items {
|
||||||
|
linkedPod := &runnerLinkedPodList.Items[i]
|
||||||
|
if !linkedPod.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Deleting container hooks runner-linked pod", "name", linkedPod.Name)
|
||||||
|
if err := r.Delete(ctx, linkedPod); err != nil && !kerrors.IsNotFound(err) {
|
||||||
|
errs = append(errs, fmt.Errorf("failed to delete runner linked pod %q: %v", linkedPod.Name, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, multierr.Combine(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EphemeralRunnerReconciler) cleanupRunnerLinkedSecrets(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) (done bool, err error) {
|
||||||
|
runnerLinkedLabels := client.MatchingLabels(
|
||||||
|
map[string]string{
|
||||||
|
"runner-pod": ephemeralRunner.ObjectMeta.Name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
var runnerLinkedSecretList corev1.SecretList
|
||||||
|
err = r.List(ctx, &runnerLinkedSecretList, client.InNamespace(ephemeralRunner.Namespace), runnerLinkedLabels)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to list runner-linked secrets: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runnerLinkedSecretList.Items) == 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Deleting container hooks runner-linked secrets", "count", len(runnerLinkedSecretList.Items))
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
for i := range runnerLinkedSecretList.Items {
|
||||||
|
s := &runnerLinkedSecretList.Items[i]
|
||||||
|
if !s.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Deleting container hooks runner-linked secret", "name", s.Name)
|
||||||
|
if err := r.Delete(ctx, s); err != nil && !kerrors.IsNotFound(err) {
|
||||||
|
errs = append(errs, fmt.Errorf("failed to delete runner linked secret %q: %v", s.Name, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, multierr.Combine(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EphemeralRunnerReconciler) markAsFailed(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) error {
|
||||||
|
log.Info("Updating ephemeral runner status to Failed")
|
||||||
|
if err := patch(ctx, r.Status(), ephemeralRunner, func(obj *v1alpha1.EphemeralRunner) {
|
||||||
|
obj.Status.Phase = corev1.PodFailed
|
||||||
|
obj.Status.Reason = "TooManyPodFailures"
|
||||||
|
obj.Status.Message = "Pod has failed to start more than 5 times"
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("failed to update ephemeral runner status Phase/Message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Removing the runner from the service")
|
||||||
|
if err := r.deleteRunnerFromService(ctx, ephemeralRunner, log); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove the runner from service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("EphemeralRunner is marked as Failed and deleted from the service")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EphemeralRunnerReconciler) markAsFinished(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) error {
|
||||||
|
log.Info("Updating ephemeral runner status to Finished")
|
||||||
|
if err := patch(ctx, r.Status(), ephemeralRunner, func(obj *v1alpha1.EphemeralRunner) {
|
||||||
|
obj.Status.Phase = corev1.PodSucceeded
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("failed to update ephemeral runner with status finished: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("EphemeralRunner status is marked as Finished")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deletePodAsFailed is responsible for deleting the pod and updating the .Status.Failures for tracking failure count.
|
||||||
|
// It should not be responsible for setting the status to Failed.
|
||||||
|
func (r *EphemeralRunnerReconciler) deletePodAsFailed(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, pod *corev1.Pod, log logr.Logger) error {
|
||||||
|
if pod.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||||
|
log.Info("Deleting the ephemeral runner pod", "podId", pod.UID)
|
||||||
|
if err := r.Delete(ctx, pod); err != nil && !kerrors.IsNotFound(err) {
|
||||||
|
return fmt.Errorf("failed to delete pod with status failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Updating ephemeral runner status to track the failure count")
|
||||||
|
if err := patch(ctx, r.Status(), ephemeralRunner, func(obj *v1alpha1.EphemeralRunner) {
|
||||||
|
if obj.Status.Failures == nil {
|
||||||
|
obj.Status.Failures = make(map[string]bool)
|
||||||
|
}
|
||||||
|
obj.Status.Failures[string(pod.UID)] = true
|
||||||
|
obj.Status.Ready = false
|
||||||
|
obj.Status.Reason = pod.Status.Reason
|
||||||
|
obj.Status.Message = pod.Status.Message
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("failed to update ephemeral runner status: failed attempts: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("EphemeralRunner pod is deleted and status is updated with failure count")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateStatusWithRunnerConfig fetches runtime configuration needed by the runner
|
||||||
|
// This method should always set .status.runnerId and .status.runnerJITConfig
|
||||||
|
func (r *EphemeralRunnerReconciler) updateStatusWithRunnerConfig(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) (ctrl.Result, error) {
|
||||||
|
// Runner is not registered with the service. We need to register it first
|
||||||
|
log.Info("Creating ephemeral runner JIT config")
|
||||||
|
actionsClient, err := r.actionsClientFor(ctx, ephemeralRunner)
|
||||||
|
if err != nil {
|
||||||
|
return ctrl.Result{}, fmt.Errorf("failed to get actions client for generating JIT config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jitSettings := &actions.RunnerScaleSetJitRunnerSetting{
|
||||||
|
Name: ephemeralRunner.Name,
|
||||||
|
}
|
||||||
|
jitConfig, err := actionsClient.GenerateJitRunnerConfig(ctx, jitSettings, ephemeralRunner.Spec.RunnerScaleSetId)
|
||||||
|
if err != nil {
|
||||||
|
actionsError := &actions.ActionsError{}
|
||||||
|
if !errors.As(err, &actionsError) {
|
||||||
|
return ctrl.Result{}, fmt.Errorf("failed to generate JIT config with generic error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if actionsError.StatusCode != http.StatusConflict ||
|
||||||
|
!strings.Contains(actionsError.ExceptionName, "AgentExistsException") {
|
||||||
|
return ctrl.Result{}, fmt.Errorf("failed to generate JIT config with Actions service error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the runner with the name we want already exists it means:
|
||||||
|
// - We might have a name collision.
|
||||||
|
// - Our previous reconciliation loop failed to update the
|
||||||
|
// status with the runnerId and runnerJITConfig after the `GenerateJitRunnerConfig`
|
||||||
|
// created the runner registration on the service.
|
||||||
|
// We will try to get the runner and see if it's belong to this AutoScalingRunnerSet,
|
||||||
|
// if so, we can simply delete the runner registration and create a new one.
|
||||||
|
log.Info("Getting runner jit config failed with conflict error, trying to get the runner by name", "runnerName", ephemeralRunner.Name)
|
||||||
|
existingRunner, err := actionsClient.GetRunnerByName(ctx, ephemeralRunner.Name)
|
||||||
|
if err != nil {
|
||||||
|
return ctrl.Result{}, fmt.Errorf("failed to get runner by name: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if existingRunner == nil {
|
||||||
|
log.Info("Runner with the same name does not exist, re-queuing the reconciliation")
|
||||||
|
return ctrl.Result{Requeue: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Found the runner with the same name", "runnerId", existingRunner.Id, "runnerScaleSetId", existingRunner.RunnerScaleSetId)
|
||||||
|
if existingRunner.RunnerScaleSetId == ephemeralRunner.Spec.RunnerScaleSetId {
|
||||||
|
log.Info("Removing the runner with the same name")
|
||||||
|
err := actionsClient.RemoveRunner(ctx, int64(existingRunner.Id))
|
||||||
|
if err != nil {
|
||||||
|
return ctrl.Result{}, fmt.Errorf("failed to remove runner from the service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Removed the runner with the same name, re-queuing the reconciliation")
|
||||||
|
return ctrl.Result{Requeue: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Do we want to mark the ephemeral runner as failed, and let EphemeralRunnerSet to clean it up, so we can recover from this situation?
|
||||||
|
// The situation is that the EphemeralRunner's name is already used by something else to register a runner, and we can't take the control back.
|
||||||
|
return ctrl.Result{}, fmt.Errorf("runner with the same name but doesn't belong to this RunnerScaleSet: %v", err)
|
||||||
|
}
|
||||||
|
log.Info("Created ephemeral runner JIT config", "runnerId", jitConfig.Runner.Id)
|
||||||
|
|
||||||
|
log.Info("Updating ephemeral runner status with runnerId and runnerJITConfig")
|
||||||
|
err = patch(ctx, r.Status(), ephemeralRunner, func(obj *v1alpha1.EphemeralRunner) {
|
||||||
|
obj.Status.RunnerId = jitConfig.Runner.Id
|
||||||
|
obj.Status.RunnerName = jitConfig.Runner.Name
|
||||||
|
obj.Status.RunnerJITConfig = jitConfig.EncodedJITConfig
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return ctrl.Result{}, fmt.Errorf("failed to update runner status for RunnerId/RunnerName/RunnerJITConfig: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Updated ephemeral runner status with runnerId and runnerJITConfig")
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EphemeralRunnerReconciler) createPod(ctx context.Context, runner *v1alpha1.EphemeralRunner, secret *corev1.Secret, log logr.Logger) (ctrl.Result, error) {
|
||||||
|
log.Info("Creating new pod for ephemeral runner")
|
||||||
|
newPod := r.resourceBuilder.newEphemeralRunnerPod(ctx, runner, secret)
|
||||||
|
|
||||||
|
if err := ctrl.SetControllerReference(runner, newPod, r.Scheme); err != nil {
|
||||||
|
log.Error(err, "Failed to set controller reference to a new pod")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Created new pod spec for ephemeral runner")
|
||||||
|
if err := r.Create(ctx, newPod); err != nil {
|
||||||
|
log.Error(err, "Failed to create pod resource for ephemeral runner.")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Created ephemeral runner pod",
|
||||||
|
"runnerScaleSetId", runner.Spec.RunnerScaleSetId,
|
||||||
|
"runnerName", runner.Status.RunnerName,
|
||||||
|
"runnerId", runner.Status.RunnerId,
|
||||||
|
"configUrl", runner.Spec.GitHubConfigUrl,
|
||||||
|
"podName", newPod.Name)
|
||||||
|
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EphemeralRunnerReconciler) createSecret(ctx context.Context, runner *v1alpha1.EphemeralRunner, log logr.Logger) (ctrl.Result, error) {
|
||||||
|
log.Info("Creating new secret for ephemeral runner")
|
||||||
|
jitSecret := r.resourceBuilder.newEphemeralRunnerJitSecret(runner)
|
||||||
|
|
||||||
|
if err := ctrl.SetControllerReference(runner, jitSecret, r.Scheme); err != nil {
|
||||||
|
return ctrl.Result{}, fmt.Errorf("failed to set controller reference: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Created new secret spec for ephemeral runner")
|
||||||
|
if err := r.Create(ctx, jitSecret); err != nil {
|
||||||
|
return ctrl.Result{}, fmt.Errorf("failed to create jit secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Created ephemeral runner secret", "secretName", jitSecret.Name)
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateRunStatusFromPod is responsible for updating non-exiting statuses.
|
||||||
|
// It should never update phase to Failed or Succeeded
|
||||||
|
//
|
||||||
|
// The event should not be re-queued since the termination status should be set
|
||||||
|
// before proceeding with reconciliation logic
|
||||||
|
func (r *EphemeralRunnerReconciler) updateRunStatusFromPod(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, pod *corev1.Pod, log logr.Logger) error {
|
||||||
|
if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if ephemeralRunner.Status.Phase == pod.Status.Phase {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Updating ephemeral runner status with pod phase", "phase", pod.Status.Phase, "reason", pod.Status.Reason, "message", pod.Status.Message)
|
||||||
|
err := patch(ctx, r.Status(), ephemeralRunner, func(obj *v1alpha1.EphemeralRunner) {
|
||||||
|
obj.Status.Phase = pod.Status.Phase
|
||||||
|
obj.Status.Ready = obj.Status.Ready || (pod.Status.Phase == corev1.PodRunning)
|
||||||
|
obj.Status.Reason = pod.Status.Reason
|
||||||
|
obj.Status.Message = pod.Status.Message
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update runner status for Phase/Reason/Message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Updated ephemeral runner status with pod phase")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EphemeralRunnerReconciler) actionsClientFor(ctx context.Context, runner *v1alpha1.EphemeralRunner) (actions.ActionsService, error) {
|
||||||
|
secret := new(corev1.Secret)
|
||||||
|
if err := r.Get(ctx, types.NamespacedName{Namespace: runner.Namespace, Name: runner.Spec.GitHubConfigSecret}, secret); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get secret: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.ActionsClient.GetClientFromSecret(ctx, runner.Spec.GitHubConfigUrl, runner.Namespace, secret.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// runnerRegisteredWithService checks if the runner is still registered with the service
|
||||||
|
// Returns found=false and err=nil if ephemeral runner does not exist in GitHub service and should be deleted
|
||||||
|
func (r EphemeralRunnerReconciler) runnerRegisteredWithService(ctx context.Context, runner *v1alpha1.EphemeralRunner, log logr.Logger) (found bool, err error) {
|
||||||
|
actionsClient, err := r.actionsClientFor(ctx, runner)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to get Actions client for ScaleSet: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Checking if runner exists in GitHub service", "runnerId", runner.Status.RunnerId)
|
||||||
|
_, err = actionsClient.GetRunner(ctx, int64(runner.Status.RunnerId))
|
||||||
|
if err != nil {
|
||||||
|
actionsError := &actions.ActionsError{}
|
||||||
|
if !errors.As(err, &actionsError) {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if actionsError.StatusCode != http.StatusNotFound ||
|
||||||
|
!strings.Contains(actionsError.ExceptionName, "AgentNotFoundException") {
|
||||||
|
return false, fmt.Errorf("failed to check if runner exists in GitHub service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Runner does not exist in GitHub service", "runnerId", runner.Status.RunnerId)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Runner exists in GitHub service", "runnerId", runner.Status.RunnerId)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EphemeralRunnerReconciler) deleteRunnerFromService(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) error {
|
||||||
|
client, err := r.actionsClientFor(ctx, ephemeralRunner)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get actions client for runner: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Removing runner from the service", "runnerId", ephemeralRunner.Status.RunnerId)
|
||||||
|
err = client.RemoveRunner(ctx, int64(ephemeralRunner.Status.RunnerId))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to remove runner from the service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Removed runner from the service", "runnerId", ephemeralRunner.Status.RunnerId)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupWithManager sets up the controller with the Manager.
|
||||||
|
func (r *EphemeralRunnerReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
// TODO(nikola-jokic): Add indexing and filtering fields on corev1.Pod{}
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(&v1alpha1.EphemeralRunner{}).
|
||||||
|
Owns(&corev1.Pod{}).
|
||||||
|
Owns(&corev1.Secret{}).
|
||||||
|
WithEventFilter(predicate.ResourceVersionChangedPredicate{}).
|
||||||
|
Named("ephemeral-runner-controller").
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runnerContainerStatus(pod *corev1.Pod) *corev1.ContainerStatus {
|
||||||
|
for i := range pod.Status.ContainerStatuses {
|
||||||
|
cs := &pod.Status.ContainerStatuses[i]
|
||||||
|
if cs.Name == EphemeralRunnerContainerName {
|
||||||
|
return cs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,769 @@
|
|||||||
|
package actionsgithubcom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions/fake"
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
gh_token = "gh_token"
|
||||||
|
timeout = time.Second * 30
|
||||||
|
interval = time.Millisecond * 250
|
||||||
|
runnerImage = "ghcr.io/actions/actions-runner:latest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newExampleRunner(name, namespace, configSecretName string) *v1alpha1.EphemeralRunner {
|
||||||
|
return &v1alpha1.EphemeralRunner{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
},
|
||||||
|
Spec: v1alpha1.EphemeralRunnerSpec{
|
||||||
|
GitHubConfigUrl: "https://github.com/owner/repo",
|
||||||
|
GitHubConfigSecret: configSecretName,
|
||||||
|
RunnerScaleSetId: 1,
|
||||||
|
PodTemplateSpec: corev1.PodTemplateSpec{
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{
|
||||||
|
Name: EphemeralRunnerContainerName,
|
||||||
|
Image: runnerImage,
|
||||||
|
Command: []string{"/runner/run.sh"},
|
||||||
|
VolumeMounts: []corev1.VolumeMount{
|
||||||
|
{
|
||||||
|
Name: "runner",
|
||||||
|
MountPath: "/runner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InitContainers: []corev1.Container{
|
||||||
|
{
|
||||||
|
Name: "setup",
|
||||||
|
Image: runnerImage,
|
||||||
|
Command: []string{"sh", "-c", "cp -r /actions-runner/* /runner/"},
|
||||||
|
VolumeMounts: []corev1.VolumeMount{
|
||||||
|
{
|
||||||
|
Name: "runner",
|
||||||
|
MountPath: "/runner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Volumes: []corev1.Volume{
|
||||||
|
{
|
||||||
|
Name: "runner",
|
||||||
|
VolumeSource: corev1.VolumeSource{
|
||||||
|
EmptyDir: &corev1.EmptyDirVolumeSource{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = Describe("EphemeralRunner", func() {
|
||||||
|
|
||||||
|
Describe("Resource manipulation", func() {
|
||||||
|
var ctx context.Context
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
|
||||||
|
autoScalingNS := new(corev1.Namespace)
|
||||||
|
configSecret := new(corev1.Secret)
|
||||||
|
|
||||||
|
controller := new(EphemeralRunnerReconciler)
|
||||||
|
ephemeralRunner := new(v1alpha1.EphemeralRunner)
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
ctx, cancel = context.WithCancel(context.Background())
|
||||||
|
autoScalingNS = &corev1.Namespace{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "testns-autoscaling-runner" + RandStringRunes(5),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := k8sClient.Create(ctx, autoScalingNS)
|
||||||
|
Expect(err).To(BeNil(), "failed to create test namespace for EphemeralRunner")
|
||||||
|
|
||||||
|
configSecret = &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "github-config-secret",
|
||||||
|
Namespace: autoScalingNS.Name,
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"github_token": []byte(gh_token),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = k8sClient.Create(ctx, configSecret)
|
||||||
|
Expect(err).To(BeNil(), "failed to create config secret")
|
||||||
|
|
||||||
|
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
|
||||||
|
Namespace: autoScalingNS.Name,
|
||||||
|
MetricsBindAddress: "0",
|
||||||
|
})
|
||||||
|
Expect(err).To(BeNil(), "failed to create manager")
|
||||||
|
|
||||||
|
controller = &EphemeralRunnerReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Scheme: mgr.GetScheme(),
|
||||||
|
Log: logf.Log,
|
||||||
|
ActionsClient: fake.NewMultiClient(),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = controller.SetupWithManager(mgr)
|
||||||
|
Expect(err).To(BeNil(), "failed to setup controller")
|
||||||
|
|
||||||
|
ephemeralRunner = newExampleRunner("test-runner", autoScalingNS.Name, configSecret.Name)
|
||||||
|
err = k8sClient.Create(ctx, ephemeralRunner)
|
||||||
|
Expect(err).To(BeNil(), "failed to create ephemeral runner")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
err := mgr.Start(ctx)
|
||||||
|
Expect(err).To(BeNil(), "failed to start manager")
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := k8sClient.Delete(ctx, autoScalingNS)
|
||||||
|
Expect(err).To(BeNil(), "failed to delete test namespace for EphemeralRunner")
|
||||||
|
})
|
||||||
|
|
||||||
|
It("It should create/add all required resources for EphemeralRunner (finalizer, jit secret)", func() {
|
||||||
|
created := new(v1alpha1.EphemeralRunner)
|
||||||
|
// Check if finalizer is added
|
||||||
|
Eventually(
|
||||||
|
func() (string, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, created)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(created.Finalizers) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return created.Finalizers[0], nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(ephemeralRunnerFinalizerName))
|
||||||
|
|
||||||
|
Eventually(
|
||||||
|
func() (bool, error) {
|
||||||
|
secret := new(corev1.Secret)
|
||||||
|
if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, secret); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := secret.Data[jitTokenKey]
|
||||||
|
return ok, nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(true))
|
||||||
|
|
||||||
|
Eventually(
|
||||||
|
func() (string, error) {
|
||||||
|
pod := new(corev1.Pod)
|
||||||
|
if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pod.Name, nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(ephemeralRunner.Name))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("It should re-create pod on failure", func() {
|
||||||
|
pod := new(corev1.Pod)
|
||||||
|
Eventually(func() (bool, error) {
|
||||||
|
if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}).Should(BeEquivalentTo(true))
|
||||||
|
|
||||||
|
err := k8sClient.Delete(ctx, pod)
|
||||||
|
Expect(err).To(BeNil(), "failed to delete pod")
|
||||||
|
|
||||||
|
pod = new(corev1.Pod)
|
||||||
|
Eventually(func() (bool, error) {
|
||||||
|
if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(true))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("It should clean up resources when deleted", func() {
|
||||||
|
// wait for pod to be created
|
||||||
|
pod := new(corev1.Pod)
|
||||||
|
Eventually(func() (bool, error) {
|
||||||
|
if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}).Should(BeEquivalentTo(true))
|
||||||
|
|
||||||
|
// create runner-linked pod
|
||||||
|
runnerLinkedPod := &corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-runner-linked-pod",
|
||||||
|
Namespace: ephemeralRunner.Namespace,
|
||||||
|
Labels: map[string]string{
|
||||||
|
"runner-pod": ephemeralRunner.Name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{
|
||||||
|
Name: "runner-linked-container",
|
||||||
|
Image: "ubuntu:latest",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := k8sClient.Create(ctx, runnerLinkedPod)
|
||||||
|
Expect(err).To(BeNil(), "failed to create runner linked pod")
|
||||||
|
Eventually(
|
||||||
|
func() (bool, error) {
|
||||||
|
pod := new(corev1.Pod)
|
||||||
|
if err := k8sClient.Get(ctx, client.ObjectKey{Name: runnerLinkedPod.Name, Namespace: runnerLinkedPod.Namespace}, pod); err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(true))
|
||||||
|
|
||||||
|
// create runner linked secret
|
||||||
|
runnerLinkedSecret := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-runner-linked-secret",
|
||||||
|
Namespace: ephemeralRunner.Namespace,
|
||||||
|
Labels: map[string]string{
|
||||||
|
"runner-pod": ephemeralRunner.Name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{"test": []byte("test")},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = k8sClient.Create(ctx, runnerLinkedSecret)
|
||||||
|
Expect(err).To(BeNil(), "failed to create runner linked secret")
|
||||||
|
Eventually(
|
||||||
|
func() (bool, error) {
|
||||||
|
secret := new(corev1.Secret)
|
||||||
|
if err := k8sClient.Get(ctx, client.ObjectKey{Name: runnerLinkedSecret.Name, Namespace: runnerLinkedSecret.Namespace}, secret); err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(true))
|
||||||
|
|
||||||
|
err = k8sClient.Delete(ctx, ephemeralRunner)
|
||||||
|
Expect(err).To(BeNil(), "failed to delete ephemeral runner")
|
||||||
|
|
||||||
|
Eventually(
|
||||||
|
func() (bool, error) {
|
||||||
|
pod := new(corev1.Pod)
|
||||||
|
err = k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod)
|
||||||
|
if err == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return kerrors.IsNotFound(err), nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(true))
|
||||||
|
|
||||||
|
Eventually(
|
||||||
|
func() (bool, error) {
|
||||||
|
secret := new(corev1.Secret)
|
||||||
|
err = k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, secret)
|
||||||
|
if err == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return kerrors.IsNotFound(err), nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(true))
|
||||||
|
|
||||||
|
Eventually(
|
||||||
|
func() (bool, error) {
|
||||||
|
pod := new(corev1.Pod)
|
||||||
|
err = k8sClient.Get(ctx, client.ObjectKey{Name: runnerLinkedPod.Name, Namespace: runnerLinkedPod.Namespace}, pod)
|
||||||
|
if err == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return kerrors.IsNotFound(err), nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(true))
|
||||||
|
|
||||||
|
Eventually(
|
||||||
|
func() (bool, error) {
|
||||||
|
secret := new(corev1.Secret)
|
||||||
|
err = k8sClient.Get(ctx, client.ObjectKey{Name: runnerLinkedSecret.Name, Namespace: runnerLinkedSecret.Namespace}, secret)
|
||||||
|
if err == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return kerrors.IsNotFound(err), nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(true))
|
||||||
|
|
||||||
|
Eventually(
|
||||||
|
func() (bool, error) {
|
||||||
|
secret := new(corev1.Secret)
|
||||||
|
err = k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, secret)
|
||||||
|
if err == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return kerrors.IsNotFound(err), nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(true))
|
||||||
|
|
||||||
|
Eventually(
|
||||||
|
func() (bool, error) {
|
||||||
|
updated := new(v1alpha1.EphemeralRunner)
|
||||||
|
err = k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated)
|
||||||
|
if err == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return kerrors.IsNotFound(err), nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(true))
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
It("It should eventually have runner id set", func() {
|
||||||
|
Eventually(
|
||||||
|
func() (int, error) {
|
||||||
|
updatedEphemeralRunner := new(v1alpha1.EphemeralRunner)
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updatedEphemeralRunner)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return updatedEphemeralRunner.Status.RunnerId, nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeNumerically(">", 0))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("It should patch the ephemeral runner non terminating status", func() {
|
||||||
|
pod := new(corev1.Pod)
|
||||||
|
Eventually(
|
||||||
|
func() (bool, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(true))
|
||||||
|
|
||||||
|
for _, phase := range []corev1.PodPhase{corev1.PodRunning, corev1.PodPending} {
|
||||||
|
podCopy := pod.DeepCopy()
|
||||||
|
pod.Status.Phase = phase
|
||||||
|
// set container state to force status update
|
||||||
|
pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses, corev1.ContainerStatus{
|
||||||
|
Name: EphemeralRunnerContainerName,
|
||||||
|
State: corev1.ContainerState{},
|
||||||
|
})
|
||||||
|
err := k8sClient.Status().Patch(ctx, pod, client.MergeFrom(podCopy))
|
||||||
|
Expect(err).To(BeNil(), "failed to patch pod status")
|
||||||
|
|
||||||
|
Eventually(
|
||||||
|
func() (corev1.PodPhase, error) {
|
||||||
|
updated := new(v1alpha1.EphemeralRunner)
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return updated.Status.Phase, nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(phase))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
It("It should not update phase if container state does not exist", func() {
|
||||||
|
pod := new(corev1.Pod)
|
||||||
|
Eventually(
|
||||||
|
func() (bool, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(true))
|
||||||
|
|
||||||
|
pod.Status.Phase = corev1.PodRunning
|
||||||
|
err := k8sClient.Status().Update(ctx, pod)
|
||||||
|
Expect(err).To(BeNil(), "failed to patch pod status")
|
||||||
|
|
||||||
|
Consistently(
|
||||||
|
func() (corev1.PodPhase, error) {
|
||||||
|
updated := new(v1alpha1.EphemeralRunner)
|
||||||
|
if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated); err != nil {
|
||||||
|
return corev1.PodUnknown, err
|
||||||
|
}
|
||||||
|
return updated.Status.Phase, nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
).Should(BeEquivalentTo(""))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("It should not re-create pod indefinitely", func() {
|
||||||
|
pod := new(corev1.Pod)
|
||||||
|
failures := 0
|
||||||
|
for i := 0; i < 6; i++ {
|
||||||
|
Eventually(
|
||||||
|
func() (bool, error) {
|
||||||
|
if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(true))
|
||||||
|
|
||||||
|
pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses, corev1.ContainerStatus{
|
||||||
|
Name: EphemeralRunnerContainerName,
|
||||||
|
State: corev1.ContainerState{
|
||||||
|
Terminated: &corev1.ContainerStateTerminated{
|
||||||
|
ExitCode: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
err := k8sClient.Status().Update(ctx, pod)
|
||||||
|
Expect(err).To(BeNil(), "Failed to update pod status")
|
||||||
|
|
||||||
|
failures++
|
||||||
|
|
||||||
|
updated := new(v1alpha1.EphemeralRunner)
|
||||||
|
Eventually(func() (bool, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return len(updated.Status.Failures) == failures, nil
|
||||||
|
}, timeout, interval).Should(BeEquivalentTo(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
Eventually(func() (bool, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod)
|
||||||
|
if err == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return kerrors.IsNotFound(err), nil
|
||||||
|
}, timeout, interval).Should(BeEquivalentTo(true))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("It should re-create pod on eviction", func() {
|
||||||
|
pod := new(corev1.Pod)
|
||||||
|
Eventually(
|
||||||
|
func() (bool, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(true))
|
||||||
|
|
||||||
|
pod.Status.Phase = corev1.PodFailed
|
||||||
|
pod.Status.Reason = "Evicted"
|
||||||
|
pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses, corev1.ContainerStatus{
|
||||||
|
Name: EphemeralRunnerContainerName,
|
||||||
|
State: corev1.ContainerState{},
|
||||||
|
})
|
||||||
|
err := k8sClient.Status().Update(ctx, pod)
|
||||||
|
Expect(err).To(BeNil(), "failed to patch pod status")
|
||||||
|
|
||||||
|
updated := new(v1alpha1.EphemeralRunner)
|
||||||
|
Eventually(func() (bool, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return len(updated.Status.Failures) == 1, nil
|
||||||
|
}, timeout, interval).Should(BeEquivalentTo(true))
|
||||||
|
|
||||||
|
// should re-create after failure
|
||||||
|
Eventually(
|
||||||
|
func() (bool, error) {
|
||||||
|
pod := new(corev1.Pod)
|
||||||
|
if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(true))
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
It("It should re-create pod on exit status 0, but runner exists within the service", func() {
|
||||||
|
pod := new(corev1.Pod)
|
||||||
|
Eventually(
|
||||||
|
func() (bool, error) {
|
||||||
|
if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(true))
|
||||||
|
|
||||||
|
pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses, corev1.ContainerStatus{
|
||||||
|
Name: EphemeralRunnerContainerName,
|
||||||
|
State: corev1.ContainerState{
|
||||||
|
Terminated: &corev1.ContainerStateTerminated{
|
||||||
|
ExitCode: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
err := k8sClient.Status().Update(ctx, pod)
|
||||||
|
Expect(err).To(BeNil(), "failed to update pod status")
|
||||||
|
|
||||||
|
updated := new(v1alpha1.EphemeralRunner)
|
||||||
|
Eventually(func() (bool, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return len(updated.Status.Failures) == 1, nil
|
||||||
|
}, timeout, interval).Should(BeEquivalentTo(true))
|
||||||
|
|
||||||
|
// should re-create after failure
|
||||||
|
Eventually(
|
||||||
|
func() (bool, error) {
|
||||||
|
pod := new(corev1.Pod)
|
||||||
|
if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(true))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("It should not set the phase to succeeded without pod termination status", func() {
|
||||||
|
pod := new(corev1.Pod)
|
||||||
|
Eventually(
|
||||||
|
func() (bool, error) {
|
||||||
|
if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(true))
|
||||||
|
|
||||||
|
// first set phase to running
|
||||||
|
pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses, corev1.ContainerStatus{
|
||||||
|
Name: EphemeralRunnerContainerName,
|
||||||
|
State: corev1.ContainerState{
|
||||||
|
Running: &corev1.ContainerStateRunning{
|
||||||
|
StartedAt: metav1.Now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
pod.Status.Phase = corev1.PodRunning
|
||||||
|
err := k8sClient.Status().Update(ctx, pod)
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
|
||||||
|
Eventually(
|
||||||
|
func() (corev1.PodPhase, error) {
|
||||||
|
updated := new(v1alpha1.EphemeralRunner)
|
||||||
|
if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return updated.Status.Phase, nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
interval,
|
||||||
|
).Should(BeEquivalentTo(corev1.PodRunning))
|
||||||
|
|
||||||
|
// set phase to succeeded
|
||||||
|
pod.Status.Phase = corev1.PodSucceeded
|
||||||
|
err = k8sClient.Status().Update(ctx, pod)
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
|
||||||
|
Consistently(
|
||||||
|
func() (corev1.PodPhase, error) {
|
||||||
|
updated := new(v1alpha1.EphemeralRunner)
|
||||||
|
if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return updated.Status.Phase, nil
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
).Should(BeEquivalentTo(corev1.PodRunning))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("Checking the API", func() {
|
||||||
|
var ctx context.Context
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
|
||||||
|
autoScalingNS := new(corev1.Namespace)
|
||||||
|
configSecret := new(corev1.Secret)
|
||||||
|
|
||||||
|
var mgr manager.Manager
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
ctx, cancel = context.WithCancel(context.Background())
|
||||||
|
autoScalingNS = &corev1.Namespace{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "testns-autoscaling-runner" + RandStringRunes(5),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := k8sClient.Create(ctx, autoScalingNS)
|
||||||
|
Expect(err).To(BeNil(), "failed to create test namespace for EphemeralRunner")
|
||||||
|
|
||||||
|
configSecret = &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "github-config-secret",
|
||||||
|
Namespace: autoScalingNS.Name,
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"github_token": []byte(gh_token),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = k8sClient.Create(ctx, configSecret)
|
||||||
|
Expect(err).To(BeNil(), "failed to create config secret")
|
||||||
|
|
||||||
|
mgr, err = ctrl.NewManager(cfg, ctrl.Options{
|
||||||
|
Namespace: autoScalingNS.Name,
|
||||||
|
MetricsBindAddress: "0",
|
||||||
|
})
|
||||||
|
Expect(err).To(BeNil(), "failed to create manager")
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := k8sClient.Delete(ctx, autoScalingNS)
|
||||||
|
Expect(err).To(BeNil(), "failed to delete test namespace for EphemeralRunner")
|
||||||
|
})
|
||||||
|
|
||||||
|
It("It should set the Phase to Succeeded", func() {
|
||||||
|
controller := &EphemeralRunnerReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Scheme: mgr.GetScheme(),
|
||||||
|
Log: logf.Log,
|
||||||
|
ActionsClient: fake.NewMultiClient(
|
||||||
|
fake.WithDefaultClient(
|
||||||
|
fake.NewFakeClient(
|
||||||
|
fake.WithGetRunner(
|
||||||
|
nil,
|
||||||
|
&actions.ActionsError{
|
||||||
|
StatusCode: http.StatusNotFound,
|
||||||
|
ExceptionName: "AgentNotFoundException",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := controller.SetupWithManager(mgr)
|
||||||
|
Expect(err).To(BeNil(), "failed to setup controller")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
err := mgr.Start(ctx)
|
||||||
|
Expect(err).To(BeNil(), "failed to start manager")
|
||||||
|
}()
|
||||||
|
|
||||||
|
ephemeralRunner := newExampleRunner("test-runner", autoScalingNS.Name, configSecret.Name)
|
||||||
|
|
||||||
|
err = k8sClient.Create(ctx, ephemeralRunner)
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
|
||||||
|
pod := new(corev1.Pod)
|
||||||
|
Eventually(func() (bool, error) {
|
||||||
|
if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}, timeout, interval).Should(BeEquivalentTo(true))
|
||||||
|
|
||||||
|
pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses, corev1.ContainerStatus{
|
||||||
|
Name: EphemeralRunnerContainerName,
|
||||||
|
State: corev1.ContainerState{
|
||||||
|
Terminated: &corev1.ContainerStateTerminated{
|
||||||
|
ExitCode: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
err = k8sClient.Status().Update(ctx, pod)
|
||||||
|
Expect(err).To(BeNil(), "failed to update pod status")
|
||||||
|
|
||||||
|
updated := new(v1alpha1.EphemeralRunner)
|
||||||
|
Eventually(func() (corev1.PodPhase, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return updated.Status.Phase, nil
|
||||||
|
}, timeout, interval).Should(BeEquivalentTo(corev1.PodSucceeded))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
463
controllers/actions.github.com/ephemeralrunnerset_controller.go
Normal file
463
controllers/actions.github.com/ephemeralrunnerset_controller.go
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 The actions-runner-controller authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package actionsgithubcom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
"go.uber.org/multierr"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ephemeralRunnerSetReconcilerOwnerKey = ".metadata.controller"
|
||||||
|
ephemeralRunnerSetFinalizerName = "ephemeralrunner.actions.github.com/finalizer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EphemeralRunnerSetReconciler reconciles a EphemeralRunnerSet object
|
||||||
|
type EphemeralRunnerSetReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Log logr.Logger
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
ActionsClient actions.MultiClient
|
||||||
|
|
||||||
|
resourceBuilder resourceBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
//+kubebuilder:rbac:groups=actions.github.com,resources=ephemeralrunnersets,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
//+kubebuilder:rbac:groups=actions.github.com,resources=ephemeralrunnersets/status,verbs=get;update;patch
|
||||||
|
|
||||||
|
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||||
|
// move the current state of the cluster closer to the desired state.
|
||||||
|
//
|
||||||
|
// The responsibility of this controller is to bring the state to the desired one, but it should
|
||||||
|
// avoid patching itself, because of the frequent patches that the listener is doing.
|
||||||
|
// The safe point where we can patch the resource is when we are reacting on finalizer.
|
||||||
|
// Then, the listener should be deleted first, to allow controller clean up resources without interruptions
|
||||||
|
//
|
||||||
|
// The resource should be created with finalizer. To leave it to this controller to add it, we would
|
||||||
|
// risk the same issue of patching the status. Responsibility of this controller should only
|
||||||
|
// be to bring the count of EphemeralRunners to the desired one, not to patch this resource
|
||||||
|
// until it is safe to do so
|
||||||
|
func (r *EphemeralRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
|
log := r.Log.WithValues("ephemeralrunnerset", req.NamespacedName)
|
||||||
|
|
||||||
|
ephemeralRunnerSet := new(v1alpha1.EphemeralRunnerSet)
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, ephemeralRunnerSet); err != nil {
|
||||||
|
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Requested deletion does not need reconciled.
|
||||||
|
if !ephemeralRunnerSet.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||||
|
if controllerutil.ContainsFinalizer(ephemeralRunnerSet, ephemeralRunnerSetFinalizerName) {
|
||||||
|
log.Info("Deleting resources")
|
||||||
|
done, err := r.cleanUpEphemeralRunners(ctx, ephemeralRunnerSet, log)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err, "Failed to clean up EphemeralRunners")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
if !done {
|
||||||
|
log.Info("Waiting for resources to be deleted")
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Removing finalizer")
|
||||||
|
if err := patch(ctx, r.Client, ephemeralRunnerSet, func(obj *v1alpha1.EphemeralRunnerSet) {
|
||||||
|
controllerutil.RemoveFinalizer(obj, ephemeralRunnerSetFinalizerName)
|
||||||
|
}); err != nil && !kerrors.IsNotFound(err) {
|
||||||
|
log.Error(err, "Failed to update ephemeral runner set with removed finalizer")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Successfully removed finalizer after cleanup")
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add finalizer if not present
|
||||||
|
if !controllerutil.ContainsFinalizer(ephemeralRunnerSet, ephemeralRunnerSetFinalizerName) {
|
||||||
|
log.Info("Adding finalizer")
|
||||||
|
if err := patch(ctx, r.Client, ephemeralRunnerSet, func(obj *v1alpha1.EphemeralRunnerSet) {
|
||||||
|
controllerutil.AddFinalizer(obj, ephemeralRunnerSetFinalizerName)
|
||||||
|
}); err != nil {
|
||||||
|
log.Error(err, "Failed to update ephemeral runner set with finalizer added")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Successfully added finalizer")
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all EphemeralRunner with matching namespace and own by this EphemeralRunnerSet.
|
||||||
|
ephemeralRunnerList := new(v1alpha1.EphemeralRunnerList)
|
||||||
|
err := r.List(
|
||||||
|
ctx,
|
||||||
|
ephemeralRunnerList,
|
||||||
|
client.InNamespace(req.Namespace),
|
||||||
|
client.MatchingFields{ephemeralRunnerSetReconcilerOwnerKey: req.Name},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err, "Unable to list child ephemeral runners")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingEphemeralRunners, runningEphemeralRunners, finishedEphemeralRunners, failedEphemeralRunners, deletingEphemeralRunners := categorizeEphemeralRunners(ephemeralRunnerList)
|
||||||
|
|
||||||
|
log.Info("Ephemeral runner counts",
|
||||||
|
"pending", len(pendingEphemeralRunners),
|
||||||
|
"running", len(runningEphemeralRunners),
|
||||||
|
"finished", len(finishedEphemeralRunners),
|
||||||
|
"failed", len(failedEphemeralRunners),
|
||||||
|
"deleting", len(deletingEphemeralRunners),
|
||||||
|
)
|
||||||
|
|
||||||
|
// cleanup finished runners and proceed
|
||||||
|
var errs []error
|
||||||
|
for i := range finishedEphemeralRunners {
|
||||||
|
log.Info("Deleting finished ephemeral runner", "name", finishedEphemeralRunners[i].Name)
|
||||||
|
if err := r.Delete(ctx, finishedEphemeralRunners[i]); err != nil {
|
||||||
|
if !kerrors.IsNotFound(err) {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
mergedErrs := multierr.Combine(errs...)
|
||||||
|
log.Error(mergedErrs, "Failed to delete finished ephemeral runners")
|
||||||
|
return ctrl.Result{}, mergedErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
total := len(pendingEphemeralRunners) + len(runningEphemeralRunners) + len(failedEphemeralRunners)
|
||||||
|
log.Info("Scaling comparison", "current", total, "desired", ephemeralRunnerSet.Spec.Replicas)
|
||||||
|
switch {
|
||||||
|
case total < ephemeralRunnerSet.Spec.Replicas: // Handle scale up
|
||||||
|
count := ephemeralRunnerSet.Spec.Replicas - total
|
||||||
|
log.Info("Creating new ephemeral runners (scale up)", "count", count)
|
||||||
|
if err := r.createEphemeralRunners(ctx, ephemeralRunnerSet, count, log); err != nil {
|
||||||
|
log.Error(err, "failed to make ephemeral runner")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
case total > ephemeralRunnerSet.Spec.Replicas: // Handle scale down scenario.
|
||||||
|
count := total - ephemeralRunnerSet.Spec.Replicas
|
||||||
|
log.Info("Deleting ephemeral runners (scale down)", "count", count)
|
||||||
|
if err := r.deleteIdleEphemeralRunners(ctx, ephemeralRunnerSet, pendingEphemeralRunners, runningEphemeralRunners, count, log); err != nil {
|
||||||
|
log.Error(err, "failed to delete idle runners")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the status if needed.
|
||||||
|
if ephemeralRunnerSet.Status.CurrentReplicas != total {
|
||||||
|
log.Info("Updating status with current runners count", "count", total)
|
||||||
|
if err := patch(ctx, r.Status(), ephemeralRunnerSet, func(obj *v1alpha1.EphemeralRunnerSet) {
|
||||||
|
obj.Status.CurrentReplicas = total
|
||||||
|
}); err != nil {
|
||||||
|
log.Error(err, "Failed to update status with current runners count")
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EphemeralRunnerSetReconciler) cleanUpEphemeralRunners(ctx context.Context, ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet, log logr.Logger) (done bool, err error) {
|
||||||
|
ephemeralRunnerList := new(v1alpha1.EphemeralRunnerList)
|
||||||
|
err = r.List(ctx, ephemeralRunnerList, client.InNamespace(ephemeralRunnerSet.Namespace), client.MatchingFields{ephemeralRunnerSetReconcilerOwnerKey: ephemeralRunnerSet.Name})
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to list child ephemeral runners: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// only if there are no ephemeral runners left, return true
|
||||||
|
if len(ephemeralRunnerList.Items) == 0 {
|
||||||
|
log.Info("All ephemeral runners are deleted")
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingEphemeralRunners, runningEphemeralRunners, finishedEphemeralRunners, failedEphemeralRunners, deletingEphemeralRunners := categorizeEphemeralRunners(ephemeralRunnerList)
|
||||||
|
|
||||||
|
log.Info("Clean up runner counts",
|
||||||
|
"pending", len(pendingEphemeralRunners),
|
||||||
|
"running", len(runningEphemeralRunners),
|
||||||
|
"finished", len(finishedEphemeralRunners),
|
||||||
|
"failed", len(failedEphemeralRunners),
|
||||||
|
"deleting", len(deletingEphemeralRunners),
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Info("Cleanup finished or failed ephemeral runners")
|
||||||
|
var errs []error
|
||||||
|
for _, ephemeralRunner := range append(finishedEphemeralRunners, failedEphemeralRunners...) {
|
||||||
|
log.Info("Deleting ephemeral runner", "name", ephemeralRunner.Name)
|
||||||
|
if err := r.Delete(ctx, ephemeralRunner); err != nil && !kerrors.IsNotFound(err) {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
mergedErrs := multierr.Combine(errs...)
|
||||||
|
log.Error(mergedErrs, "Failed to delete ephemeral runners")
|
||||||
|
return false, mergedErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// avoid fetching the client if we have nothing left to do
|
||||||
|
if len(runningEphemeralRunners) == 0 && len(pendingEphemeralRunners) == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
actionsClient, err := r.actionsClientFor(ctx, ephemeralRunnerSet)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Cleanup pending or running ephemeral runners")
|
||||||
|
errs = errs[0:0]
|
||||||
|
for _, ephemeralRunner := range append(pendingEphemeralRunners, runningEphemeralRunners...) {
|
||||||
|
log.Info("Removing the ephemeral runner from the service", "name", ephemeralRunner.Name)
|
||||||
|
_, err := r.deleteEphemeralRunnerWithActionsClient(ctx, ephemeralRunner, actionsClient, log)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
mergedErrs := multierr.Combine(errs...)
|
||||||
|
log.Error(mergedErrs, "Failed to remove ephemeral runners from the service")
|
||||||
|
return false, mergedErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createEphemeralRunners provisions `count` number of v1alpha1.EphemeralRunner resources in the cluster.
|
||||||
|
func (r *EphemeralRunnerSetReconciler) createEphemeralRunners(ctx context.Context, runnerSet *v1alpha1.EphemeralRunnerSet, count int, log logr.Logger) error {
|
||||||
|
// Track multiple errors at once and return the bundle.
|
||||||
|
errs := make([]error, 0)
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
ephemeralRunner := r.resourceBuilder.newEphemeralRunner(runnerSet)
|
||||||
|
|
||||||
|
// Make sure that we own the resource we create.
|
||||||
|
if err := ctrl.SetControllerReference(runnerSet, ephemeralRunner, r.Scheme); err != nil {
|
||||||
|
log.Error(err, "failed to set controller reference on ephemeral runner")
|
||||||
|
errs = append(errs, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Creating new ephemeral runner", "progress", i+1, "total", count)
|
||||||
|
if err := r.Create(ctx, ephemeralRunner); err != nil {
|
||||||
|
log.Error(err, "failed to make ephemeral runner")
|
||||||
|
errs = append(errs, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Created new ephemeral runner", "runner", ephemeralRunner.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return multierr.Combine(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteIdleEphemeralRunners try to deletes `count` number of v1alpha1.EphemeralRunner resources in the cluster.
|
||||||
|
// It will only delete `v1alpha1.EphemeralRunner` that has registered with Actions service
|
||||||
|
// which has a `v1alpha1.EphemeralRunner.Status.RunnerId` set.
|
||||||
|
// So, it is possible that this function will not delete enough ephemeral runners
|
||||||
|
// if there are not enough ephemeral runners that have registered with Actions service.
|
||||||
|
// When this happens, the next reconcile loop will try to delete the remaining ephemeral runners
|
||||||
|
// after we get notified by any of the `v1alpha1.EphemeralRunner.Status` updates.
|
||||||
|
func (r *EphemeralRunnerSetReconciler) deleteIdleEphemeralRunners(ctx context.Context, ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet, pendingEphemeralRunners, runningEphemeralRunners []*v1alpha1.EphemeralRunner, count int, log logr.Logger) error {
|
||||||
|
runners := newEphemeralRunnerStepper(pendingEphemeralRunners, runningEphemeralRunners)
|
||||||
|
if runners.len() == 0 {
|
||||||
|
log.Info("No pending or running ephemeral runners running at this time for scale down")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
actionsClient, err := r.actionsClientFor(ctx, ephemeralRunnerSet)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create actions client for ephemeral runner replica set: %v", err)
|
||||||
|
}
|
||||||
|
var errs []error
|
||||||
|
deletedCount := 0
|
||||||
|
for runners.next() {
|
||||||
|
ephemeralRunner := runners.object()
|
||||||
|
if ephemeralRunner.Status.RunnerId == 0 {
|
||||||
|
log.Info("Skipping ephemeral runner since it is not registered yet", "name", ephemeralRunner.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ephemeralRunner.Status.JobRequestId > 0 {
|
||||||
|
log.Info("Skipping ephemeral runner since it is running a job", "name", ephemeralRunner.Name, "jobRequestId", ephemeralRunner.Status.JobRequestId)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Removing the idle ephemeral runner", "name", ephemeralRunner.Name)
|
||||||
|
ok, err := r.deleteEphemeralRunnerWithActionsClient(ctx, ephemeralRunner, actionsClient, log)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
deletedCount++
|
||||||
|
if deletedCount == count {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return multierr.Combine(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EphemeralRunnerSetReconciler) deleteEphemeralRunnerWithActionsClient(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, actionsClient actions.ActionsService, log logr.Logger) (bool, error) {
|
||||||
|
if err := actionsClient.RemoveRunner(ctx, int64(ephemeralRunner.Status.RunnerId)); err != nil {
|
||||||
|
actionsError := &actions.ActionsError{}
|
||||||
|
if errors.As(err, &actionsError) &&
|
||||||
|
actionsError.StatusCode == http.StatusBadRequest &&
|
||||||
|
strings.Contains(actionsError.ExceptionName, "JobStillRunningException") {
|
||||||
|
// Runner is still running a job, proceed with the next one
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Deleting ephemeral runner after removing from the service", "name", ephemeralRunner.Name, "runnerId", ephemeralRunner.Status.RunnerId)
|
||||||
|
if err := r.Delete(ctx, ephemeralRunner); err != nil && !kerrors.IsNotFound(err) {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Deleted ephemeral runner", "name", ephemeralRunner.Name, "runnerId", ephemeralRunner.Status.RunnerId)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EphemeralRunnerSetReconciler) actionsClientFor(ctx context.Context, rs *v1alpha1.EphemeralRunnerSet) (actions.ActionsService, error) {
|
||||||
|
secret := new(corev1.Secret)
|
||||||
|
if err := r.Get(ctx, types.NamespacedName{Namespace: rs.Namespace, Name: rs.Spec.EphemeralRunnerSpec.GitHubConfigSecret}, secret); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get secret: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.ActionsClient.GetClientFromSecret(ctx, rs.Spec.EphemeralRunnerSpec.GitHubConfigUrl, rs.Namespace, secret.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupWithManager sets up the controller with the Manager.
|
||||||
|
func (r *EphemeralRunnerSetReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
// Index EphemeralRunner owned by EphemeralRunnerSet so we can perform faster look ups.
|
||||||
|
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &v1alpha1.EphemeralRunner{}, ephemeralRunnerSetReconcilerOwnerKey, func(rawObj client.Object) []string {
|
||||||
|
groupVersion := v1alpha1.GroupVersion.String()
|
||||||
|
|
||||||
|
// grab the job object, extract the owner...
|
||||||
|
ephemeralRunner := rawObj.(*v1alpha1.EphemeralRunner)
|
||||||
|
owner := metav1.GetControllerOf(ephemeralRunner)
|
||||||
|
if owner == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...make sure it is owned by this controller
|
||||||
|
if owner.APIVersion != groupVersion || owner.Kind != "EphemeralRunnerSet" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...and if so, return it
|
||||||
|
return []string{owner.Name}
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(&v1alpha1.EphemeralRunnerSet{}).
|
||||||
|
Owns(&v1alpha1.EphemeralRunner{}).
|
||||||
|
WithEventFilter(predicate.ResourceVersionChangedPredicate{}).
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ephemeralRunnerStepper struct {
|
||||||
|
items []*v1alpha1.EphemeralRunner
|
||||||
|
index int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEphemeralRunnerStepper(pending, running []*v1alpha1.EphemeralRunner) *ephemeralRunnerStepper {
|
||||||
|
sort.Slice(pending, func(i, j int) bool {
|
||||||
|
return pending[i].GetCreationTimestamp().Time.Before(pending[j].GetCreationTimestamp().Time)
|
||||||
|
})
|
||||||
|
sort.Slice(running, func(i, j int) bool {
|
||||||
|
return running[i].GetCreationTimestamp().Time.Before(running[j].GetCreationTimestamp().Time)
|
||||||
|
})
|
||||||
|
|
||||||
|
return &ephemeralRunnerStepper{
|
||||||
|
items: append(pending, running...),
|
||||||
|
index: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ephemeralRunnerStepper) next() bool {
|
||||||
|
if s.index+1 < len(s.items) {
|
||||||
|
s.index++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ephemeralRunnerStepper) object() *v1alpha1.EphemeralRunner {
|
||||||
|
if s.index >= 0 && s.index < len(s.items) {
|
||||||
|
return s.items[s.index]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ephemeralRunnerStepper) len() int {
|
||||||
|
return len(s.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
func categorizeEphemeralRunners(ephemeralRunnerList *v1alpha1.EphemeralRunnerList) (pendingEphemeralRunners, runningEphemeralRunners, finishedEphemeralRunners, failedEphemeralRunners, deletingEphemeralRunners []*v1alpha1.EphemeralRunner) {
|
||||||
|
for i := range ephemeralRunnerList.Items {
|
||||||
|
r := &ephemeralRunnerList.Items[i]
|
||||||
|
if !r.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||||
|
deletingEphemeralRunners = append(deletingEphemeralRunners, r)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Status.Phase {
|
||||||
|
case corev1.PodRunning:
|
||||||
|
runningEphemeralRunners = append(runningEphemeralRunners, r)
|
||||||
|
case corev1.PodSucceeded:
|
||||||
|
finishedEphemeralRunners = append(finishedEphemeralRunners, r)
|
||||||
|
case corev1.PodFailed:
|
||||||
|
failedEphemeralRunners = append(failedEphemeralRunners, r)
|
||||||
|
default:
|
||||||
|
// Pending or no phase should be considered as pending.
|
||||||
|
//
|
||||||
|
// If field is not set, that means that the EphemeralRunner
|
||||||
|
// did not yet have chance to update the Status.Phase field.
|
||||||
|
pendingEphemeralRunners = append(pendingEphemeralRunners, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,445 @@
|
|||||||
|
package actionsgithubcom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
|
actionsv1alpha1 "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions/fake"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ephemeralRunnerSetTestTimeout = time.Second * 5
|
||||||
|
ephemeralRunnerSetTestInterval = time.Millisecond * 250
|
||||||
|
ephemeralRunnerSetTestGitHubToken = "gh_token"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Test EphemeralRunnerSet controller", func() {
|
||||||
|
var ctx context.Context
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
autoScalingNS := new(corev1.Namespace)
|
||||||
|
ephemeralRunnerSet := new(actionsv1alpha1.EphemeralRunnerSet)
|
||||||
|
configSecret := new(corev1.Secret)
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
ctx, cancel = context.WithCancel(context.TODO())
|
||||||
|
autoScalingNS = &corev1.Namespace{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "testns-autoscaling-runnerset" + RandStringRunes(5)},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := k8sClient.Create(ctx, autoScalingNS)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to create test namespace for EphemeralRunnerSet")
|
||||||
|
|
||||||
|
configSecret = &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "github-config-secret",
|
||||||
|
Namespace: autoScalingNS.Name,
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"github_token": []byte(ephemeralRunnerSetTestGitHubToken),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = k8sClient.Create(ctx, configSecret)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to create config secret")
|
||||||
|
|
||||||
|
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
|
||||||
|
Namespace: autoScalingNS.Name,
|
||||||
|
MetricsBindAddress: "0",
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to create manager")
|
||||||
|
|
||||||
|
controller := &EphemeralRunnerSetReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Scheme: mgr.GetScheme(),
|
||||||
|
Log: logf.Log,
|
||||||
|
ActionsClient: fake.NewMultiClient(),
|
||||||
|
}
|
||||||
|
err = controller.SetupWithManager(mgr)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
||||||
|
|
||||||
|
ephemeralRunnerSet = &actionsv1alpha1.EphemeralRunnerSet{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-asrs",
|
||||||
|
Namespace: autoScalingNS.Name,
|
||||||
|
},
|
||||||
|
Spec: actionsv1alpha1.EphemeralRunnerSetSpec{
|
||||||
|
EphemeralRunnerSpec: actionsv1alpha1.EphemeralRunnerSpec{
|
||||||
|
GitHubConfigUrl: "https://github.com/owner/repo",
|
||||||
|
GitHubConfigSecret: configSecret.Name,
|
||||||
|
RunnerScaleSetId: 100,
|
||||||
|
PodTemplateSpec: corev1.PodTemplateSpec{
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{
|
||||||
|
Name: "runner",
|
||||||
|
Image: "ghcr.io/actions/runner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = k8sClient.Create(ctx, ephemeralRunnerSet)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to create EphemeralRunnerSet")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
err := mgr.Start(ctx)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to start manager")
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := k8sClient.Delete(ctx, autoScalingNS)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to delete test namespace for EphemeralRunnerSet")
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("When creating a new EphemeralRunnerSet", func() {
|
||||||
|
It("It should create/add all required resources for a new EphemeralRunnerSet (finalizer)", func() {
|
||||||
|
// Check if finalizer is added
|
||||||
|
created := new(actionsv1alpha1.EphemeralRunnerSet)
|
||||||
|
Eventually(
|
||||||
|
func() (string, error) {
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, created)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(created.Finalizers) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return created.Finalizers[0], nil
|
||||||
|
},
|
||||||
|
ephemeralRunnerSetTestTimeout,
|
||||||
|
ephemeralRunnerSetTestInterval).Should(BeEquivalentTo(ephemeralRunnerSetFinalizerName), "EphemeralRunnerSet should have a finalizer")
|
||||||
|
|
||||||
|
// Check if the number of ephemeral runners are stay 0
|
||||||
|
Consistently(
|
||||||
|
func() (int, error) {
|
||||||
|
runnerList := new(actionsv1alpha1.EphemeralRunnerList)
|
||||||
|
err := k8sClient.List(ctx, runnerList, client.InNamespace(ephemeralRunnerSet.Namespace))
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(runnerList.Items), nil
|
||||||
|
},
|
||||||
|
ephemeralRunnerSetTestTimeout,
|
||||||
|
ephemeralRunnerSetTestInterval).Should(BeEquivalentTo(0), "No EphemeralRunner should be created")
|
||||||
|
|
||||||
|
// Check if the status stay 0
|
||||||
|
Consistently(
|
||||||
|
func() (int, error) {
|
||||||
|
runnerSet := new(actionsv1alpha1.EphemeralRunnerSet)
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, runnerSet)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(runnerSet.Status.CurrentReplicas), nil
|
||||||
|
},
|
||||||
|
ephemeralRunnerSetTestTimeout,
|
||||||
|
ephemeralRunnerSetTestInterval).Should(BeEquivalentTo(0), "EphemeralRunnerSet status should be 0")
|
||||||
|
|
||||||
|
// Scaling up the EphemeralRunnerSet
|
||||||
|
updated := created.DeepCopy()
|
||||||
|
updated.Spec.Replicas = 5
|
||||||
|
err := k8sClient.Update(ctx, updated)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
||||||
|
|
||||||
|
// Check if the number of ephemeral runners are created
|
||||||
|
Eventually(
|
||||||
|
func() (int, error) {
|
||||||
|
runnerList := new(actionsv1alpha1.EphemeralRunnerList)
|
||||||
|
err := k8sClient.List(ctx, runnerList, client.InNamespace(ephemeralRunnerSet.Namespace))
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(runnerList.Items), nil
|
||||||
|
},
|
||||||
|
ephemeralRunnerSetTestTimeout,
|
||||||
|
ephemeralRunnerSetTestInterval).Should(BeEquivalentTo(5), "5 EphemeralRunner should be created")
|
||||||
|
|
||||||
|
// Check if the status is updated
|
||||||
|
Eventually(
|
||||||
|
func() (int, error) {
|
||||||
|
runnerSet := new(actionsv1alpha1.EphemeralRunnerSet)
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, runnerSet)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(runnerSet.Status.CurrentReplicas), nil
|
||||||
|
},
|
||||||
|
ephemeralRunnerSetTestTimeout,
|
||||||
|
ephemeralRunnerSetTestInterval).Should(BeEquivalentTo(5), "EphemeralRunnerSet status should be 5")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("When deleting a new EphemeralRunnerSet", func() {
|
||||||
|
It("It should cleanup all resources for a deleting EphemeralRunnerSet before removing it", func() {
|
||||||
|
created := new(actionsv1alpha1.EphemeralRunnerSet)
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, created)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
||||||
|
|
||||||
|
// Scale up the EphemeralRunnerSet
|
||||||
|
updated := created.DeepCopy()
|
||||||
|
updated.Spec.Replicas = 5
|
||||||
|
err = k8sClient.Update(ctx, updated)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
||||||
|
|
||||||
|
// Wait for the EphemeralRunnerSet to be scaled up
|
||||||
|
Eventually(
|
||||||
|
func() (int, error) {
|
||||||
|
runnerList := new(actionsv1alpha1.EphemeralRunnerList)
|
||||||
|
err := k8sClient.List(ctx, runnerList, client.InNamespace(ephemeralRunnerSet.Namespace))
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(runnerList.Items), nil
|
||||||
|
},
|
||||||
|
ephemeralRunnerSetTestTimeout,
|
||||||
|
ephemeralRunnerSetTestInterval).Should(BeEquivalentTo(5), "5 EphemeralRunner should be created")
|
||||||
|
|
||||||
|
// Delete the EphemeralRunnerSet
|
||||||
|
err = k8sClient.Delete(ctx, created)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to delete EphemeralRunnerSet")
|
||||||
|
|
||||||
|
// Check if all ephemeral runners are deleted
|
||||||
|
Eventually(
|
||||||
|
func() (int, error) {
|
||||||
|
runnerList := new(actionsv1alpha1.EphemeralRunnerList)
|
||||||
|
err := k8sClient.List(ctx, runnerList, client.InNamespace(ephemeralRunnerSet.Namespace))
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(runnerList.Items), nil
|
||||||
|
},
|
||||||
|
ephemeralRunnerSetTestTimeout,
|
||||||
|
ephemeralRunnerSetTestInterval).Should(BeEquivalentTo(0), "All EphemeralRunner should be deleted")
|
||||||
|
|
||||||
|
// Check if the EphemeralRunnerSet is deleted
|
||||||
|
Eventually(
|
||||||
|
func() error {
|
||||||
|
deleted := new(actionsv1alpha1.EphemeralRunnerSet)
|
||||||
|
err = k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, deleted)
|
||||||
|
if err != nil {
|
||||||
|
if kerrors.IsNotFound(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("EphemeralRunnerSet is not deleted")
|
||||||
|
},
|
||||||
|
ephemeralRunnerSetTestTimeout,
|
||||||
|
ephemeralRunnerSetTestInterval).Should(Succeed(), "EphemeralRunnerSet should be deleted")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("When a new EphemeralRunnerSet scale up and down", func() {
|
||||||
|
It("It should delete finished EphemeralRunner and create new EphemeralRunner", func() {
|
||||||
|
created := new(actionsv1alpha1.EphemeralRunnerSet)
|
||||||
|
err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunnerSet.Name, Namespace: ephemeralRunnerSet.Namespace}, created)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
||||||
|
|
||||||
|
// Scale up the EphemeralRunnerSet
|
||||||
|
updated := created.DeepCopy()
|
||||||
|
updated.Spec.Replicas = 5
|
||||||
|
err = k8sClient.Update(ctx, updated)
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
||||||
|
|
||||||
|
// Wait for the EphemeralRunnerSet to be scaled up
|
||||||
|
runnerList := new(actionsv1alpha1.EphemeralRunnerList)
|
||||||
|
Eventually(
|
||||||
|
func() (int, error) {
|
||||||
|
err := k8sClient.List(ctx, runnerList, client.InNamespace(ephemeralRunnerSet.Namespace))
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(runnerList.Items), nil
|
||||||
|
},
|
||||||
|
ephemeralRunnerSetTestTimeout,
|
||||||
|
ephemeralRunnerSetTestInterval).Should(BeEquivalentTo(5), "5 EphemeralRunner should be created")
|
||||||
|
|
||||||
|
// Set status to simulate a configured EphemeralRunner
|
||||||
|
for i, runner := range runnerList.Items {
|
||||||
|
updatedRunner := runner.DeepCopy()
|
||||||
|
updatedRunner.Status.Phase = corev1.PodRunning
|
||||||
|
updatedRunner.Status.RunnerId = i + 100
|
||||||
|
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runner))
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark one of the EphemeralRunner as finished
|
||||||
|
finishedRunner := runnerList.Items[4].DeepCopy()
|
||||||
|
finishedRunner.Status.Phase = corev1.PodSucceeded
|
||||||
|
err = k8sClient.Status().Patch(ctx, finishedRunner, client.MergeFrom(&runnerList.Items[4]))
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
||||||
|
|
||||||
|
// Wait for the finished EphemeralRunner to be deleted
|
||||||
|
Eventually(
|
||||||
|
func() error {
|
||||||
|
runnerList := new(actionsv1alpha1.EphemeralRunnerList)
|
||||||
|
err := k8sClient.List(ctx, runnerList, client.InNamespace(ephemeralRunnerSet.Namespace))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, runner := range runnerList.Items {
|
||||||
|
if runner.Name == finishedRunner.Name {
|
||||||
|
return fmt.Errorf("EphemeralRunner is not deleted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ephemeralRunnerSetTestTimeout,
|
||||||
|
ephemeralRunnerSetTestInterval).Should(Succeed(), "Finished EphemeralRunner should be deleted")
|
||||||
|
|
||||||
|
// We should still have the EphemeralRunnerSet scale up
|
||||||
|
runnerList = new(actionsv1alpha1.EphemeralRunnerList)
|
||||||
|
Eventually(
|
||||||
|
func() (int, error) {
|
||||||
|
err := k8sClient.List(ctx, runnerList, client.InNamespace(ephemeralRunnerSet.Namespace))
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(runnerList.Items), nil
|
||||||
|
},
|
||||||
|
ephemeralRunnerSetTestTimeout,
|
||||||
|
ephemeralRunnerSetTestInterval).Should(BeEquivalentTo(5), "5 EphemeralRunner should be created")
|
||||||
|
|
||||||
|
// Set status to simulate a configured EphemeralRunner
|
||||||
|
for i, runner := range runnerList.Items {
|
||||||
|
updatedRunner := runner.DeepCopy()
|
||||||
|
updatedRunner.Status.Phase = corev1.PodRunning
|
||||||
|
updatedRunner.Status.RunnerId = i + 100
|
||||||
|
err = k8sClient.Status().Patch(ctx, updatedRunner, client.MergeFrom(&runner))
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale down the EphemeralRunnerSet
|
||||||
|
updated = created.DeepCopy()
|
||||||
|
updated.Spec.Replicas = 3
|
||||||
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(created))
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
||||||
|
|
||||||
|
// Wait for the EphemeralRunnerSet to be scaled down
|
||||||
|
runnerList = new(actionsv1alpha1.EphemeralRunnerList)
|
||||||
|
Eventually(
|
||||||
|
func() (int, error) {
|
||||||
|
err := k8sClient.List(ctx, runnerList, client.InNamespace(ephemeralRunnerSet.Namespace))
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(runnerList.Items), nil
|
||||||
|
},
|
||||||
|
ephemeralRunnerSetTestTimeout,
|
||||||
|
ephemeralRunnerSetTestInterval).Should(BeEquivalentTo(3), "3 EphemeralRunner should be created")
|
||||||
|
|
||||||
|
// We will not scale down runner that is running jobs
|
||||||
|
runningRunner := runnerList.Items[0].DeepCopy()
|
||||||
|
runningRunner.Status.JobRequestId = 1000
|
||||||
|
err = k8sClient.Status().Patch(ctx, runningRunner, client.MergeFrom(&runnerList.Items[0]))
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
||||||
|
|
||||||
|
runningRunner = runnerList.Items[1].DeepCopy()
|
||||||
|
runningRunner.Status.JobRequestId = 1001
|
||||||
|
err = k8sClient.Status().Patch(ctx, runningRunner, client.MergeFrom(&runnerList.Items[0]))
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
||||||
|
|
||||||
|
// Scale down to 1
|
||||||
|
updated = created.DeepCopy()
|
||||||
|
updated.Spec.Replicas = 1
|
||||||
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(created))
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
||||||
|
|
||||||
|
// Wait for the EphemeralRunnerSet to be scaled down to 2 since we still have 2 runner running jobs
|
||||||
|
runnerList = new(actionsv1alpha1.EphemeralRunnerList)
|
||||||
|
Eventually(
|
||||||
|
func() (int, error) {
|
||||||
|
err := k8sClient.List(ctx, runnerList, client.InNamespace(ephemeralRunnerSet.Namespace))
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(runnerList.Items), nil
|
||||||
|
},
|
||||||
|
ephemeralRunnerSetTestTimeout,
|
||||||
|
ephemeralRunnerSetTestInterval).Should(BeEquivalentTo(2), "2 EphemeralRunner should be created")
|
||||||
|
|
||||||
|
// We will not scale down failed runner
|
||||||
|
failedRunner := runnerList.Items[0].DeepCopy()
|
||||||
|
failedRunner.Status.Phase = corev1.PodFailed
|
||||||
|
err = k8sClient.Status().Patch(ctx, failedRunner, client.MergeFrom(&runnerList.Items[0]))
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
||||||
|
|
||||||
|
// Scale down to 0
|
||||||
|
updated = created.DeepCopy()
|
||||||
|
updated.Spec.Replicas = 0
|
||||||
|
err = k8sClient.Patch(ctx, updated, client.MergeFrom(created))
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
||||||
|
|
||||||
|
// We should not scale down the EphemeralRunnerSet since we still have 1 runner running job and 1 failed runner
|
||||||
|
runnerList = new(actionsv1alpha1.EphemeralRunnerList)
|
||||||
|
Consistently(
|
||||||
|
func() (int, error) {
|
||||||
|
err := k8sClient.List(ctx, runnerList, client.InNamespace(ephemeralRunnerSet.Namespace))
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(runnerList.Items), nil
|
||||||
|
},
|
||||||
|
ephemeralRunnerSetTestTimeout,
|
||||||
|
ephemeralRunnerSetTestInterval).Should(BeEquivalentTo(2), "2 EphemeralRunner should be created")
|
||||||
|
|
||||||
|
// We will scale down to 0 when the running job is completed and the failed runner is deleted
|
||||||
|
runningRunner = runnerList.Items[1].DeepCopy()
|
||||||
|
runningRunner.Status.Phase = corev1.PodSucceeded
|
||||||
|
err = k8sClient.Status().Patch(ctx, runningRunner, client.MergeFrom(&runnerList.Items[1]))
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunner")
|
||||||
|
|
||||||
|
err = k8sClient.Delete(ctx, &runnerList.Items[0])
|
||||||
|
Expect(err).NotTo(HaveOccurred(), "failed to delete EphemeralRunner")
|
||||||
|
|
||||||
|
// Wait for the EphemeralRunnerSet to be scaled down to 0
|
||||||
|
runnerList = new(actionsv1alpha1.EphemeralRunnerList)
|
||||||
|
Eventually(
|
||||||
|
func() (int, error) {
|
||||||
|
err := k8sClient.List(ctx, runnerList, client.InNamespace(ephemeralRunnerSet.Namespace))
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(runnerList.Items), nil
|
||||||
|
},
|
||||||
|
ephemeralRunnerSetTestTimeout,
|
||||||
|
ephemeralRunnerSetTestInterval).Should(BeEquivalentTo(0), "0 EphemeralRunner should be created")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
437
controllers/actions.github.com/resourcebuilder.go
Normal file
437
controllers/actions.github.com/resourcebuilder.go
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
package actionsgithubcom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
|
||||||
|
"github.com/actions/actions-runner-controller/hash"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
jitTokenKey = "jitToken"
|
||||||
|
)
|
||||||
|
|
||||||
|
type resourceBuilder struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *resourceBuilder) newScaleSetListenerPod(autoscalingListener *v1alpha1.AutoscalingListener, serviceAccount *corev1.ServiceAccount, secret *corev1.Secret) *corev1.Pod {
|
||||||
|
newLabels := map[string]string{}
|
||||||
|
newLabels[scaleSetListenerLabel] = fmt.Sprintf("%v-%v", autoscalingListener.Spec.AutoscalingRunnerSetNamespace, autoscalingListener.Spec.AutoscalingRunnerSetName)
|
||||||
|
|
||||||
|
listenerEnv := []corev1.EnvVar{
|
||||||
|
{
|
||||||
|
Name: "GITHUB_CONFIGURE_URL",
|
||||||
|
Value: autoscalingListener.Spec.GitHubConfigUrl,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "GITHUB_EPHEMERAL_RUNNER_SET_NAMESPACE",
|
||||||
|
Value: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "GITHUB_EPHEMERAL_RUNNER_SET_NAME",
|
||||||
|
Value: autoscalingListener.Spec.EphemeralRunnerSetName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "GITHUB_MAX_RUNNERS",
|
||||||
|
Value: strconv.Itoa(autoscalingListener.Spec.MaxRunners),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "GITHUB_MIN_RUNNERS",
|
||||||
|
Value: strconv.Itoa(autoscalingListener.Spec.MinRunners),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "GITHUB_RUNNER_SCALE_SET_ID",
|
||||||
|
Value: strconv.Itoa(autoscalingListener.Spec.RunnerScaleSetId),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := secret.Data["github_token"]; ok {
|
||||||
|
listenerEnv = append(listenerEnv, corev1.EnvVar{
|
||||||
|
Name: "GITHUB_TOKEN",
|
||||||
|
ValueFrom: &corev1.EnvVarSource{
|
||||||
|
SecretKeyRef: &corev1.SecretKeySelector{
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{
|
||||||
|
Name: secret.Name,
|
||||||
|
},
|
||||||
|
Key: "github_token",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := secret.Data["github_app_id"]; ok {
|
||||||
|
listenerEnv = append(listenerEnv, corev1.EnvVar{
|
||||||
|
Name: "GITHUB_APP_ID",
|
||||||
|
ValueFrom: &corev1.EnvVarSource{
|
||||||
|
SecretKeyRef: &corev1.SecretKeySelector{
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{
|
||||||
|
Name: secret.Name,
|
||||||
|
},
|
||||||
|
Key: "github_app_id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := secret.Data["github_app_installation_id"]; ok {
|
||||||
|
listenerEnv = append(listenerEnv, corev1.EnvVar{
|
||||||
|
Name: "GITHUB_APP_INSTALLATION_ID",
|
||||||
|
ValueFrom: &corev1.EnvVarSource{
|
||||||
|
SecretKeyRef: &corev1.SecretKeySelector{
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{
|
||||||
|
Name: secret.Name,
|
||||||
|
},
|
||||||
|
Key: "github_app_installation_id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := secret.Data["github_app_private_key"]; ok {
|
||||||
|
listenerEnv = append(listenerEnv, corev1.EnvVar{
|
||||||
|
Name: "GITHUB_APP_PRIVATE_KEY",
|
||||||
|
ValueFrom: &corev1.EnvVarSource{
|
||||||
|
SecretKeyRef: &corev1.SecretKeySelector{
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{
|
||||||
|
Name: secret.Name,
|
||||||
|
},
|
||||||
|
Key: "github_app_private_key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
podSpec := corev1.PodSpec{
|
||||||
|
ServiceAccountName: serviceAccount.Name,
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{
|
||||||
|
Name: name,
|
||||||
|
Image: autoscalingListener.Spec.Image,
|
||||||
|
Env: listenerEnv,
|
||||||
|
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||||
|
Command: []string{
|
||||||
|
"/github-runnerscaleset-listener",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ImagePullSecrets: autoscalingListener.Spec.ImagePullSecrets,
|
||||||
|
RestartPolicy: corev1.RestartPolicyNever,
|
||||||
|
}
|
||||||
|
|
||||||
|
newRunnerScaleSetListenerPod := &corev1.Pod{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "Pod",
|
||||||
|
APIVersion: "v1",
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: autoscalingListener.Name,
|
||||||
|
Namespace: autoscalingListener.Namespace,
|
||||||
|
Labels: newLabels,
|
||||||
|
},
|
||||||
|
Spec: podSpec,
|
||||||
|
}
|
||||||
|
|
||||||
|
return newRunnerScaleSetListenerPod
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *resourceBuilder) newEphemeralRunnerSet(autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet) (*v1alpha1.EphemeralRunnerSet, error) {
|
||||||
|
runnerScaleSetId, err := strconv.Atoi(autoscalingRunnerSet.Annotations[runnerScaleSetIdKey])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
runnerSpecHash := autoscalingRunnerSet.RunnerSetSpecHash()
|
||||||
|
|
||||||
|
newLabels := map[string]string{}
|
||||||
|
newLabels[LabelKeyRunnerSpecHash] = runnerSpecHash
|
||||||
|
|
||||||
|
newEphemeralRunnerSet := &v1alpha1.EphemeralRunnerSet{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
GenerateName: autoscalingRunnerSet.ObjectMeta.Name + "-",
|
||||||
|
Namespace: autoscalingRunnerSet.ObjectMeta.Namespace,
|
||||||
|
Labels: newLabels,
|
||||||
|
},
|
||||||
|
Spec: v1alpha1.EphemeralRunnerSetSpec{
|
||||||
|
Replicas: 0,
|
||||||
|
EphemeralRunnerSpec: v1alpha1.EphemeralRunnerSpec{
|
||||||
|
RunnerScaleSetId: runnerScaleSetId,
|
||||||
|
GitHubConfigUrl: autoscalingRunnerSet.Spec.GitHubConfigUrl,
|
||||||
|
GitHubConfigSecret: autoscalingRunnerSet.Spec.GitHubConfigSecret,
|
||||||
|
Proxy: autoscalingRunnerSet.Spec.Proxy,
|
||||||
|
GitHubServerTLS: autoscalingRunnerSet.Spec.GitHubServerTLS,
|
||||||
|
PodTemplateSpec: autoscalingRunnerSet.Spec.Template,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return newEphemeralRunnerSet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *resourceBuilder) newScaleSetListenerServiceAccount(autoscalingListener *v1alpha1.AutoscalingListener) *corev1.ServiceAccount {
|
||||||
|
return &corev1.ServiceAccount{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: scaleSetListenerServiceAccountName(autoscalingListener),
|
||||||
|
Namespace: autoscalingListener.Namespace,
|
||||||
|
Labels: map[string]string{
|
||||||
|
"auto-scaling-runner-set-namespace": autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
|
||||||
|
"auto-scaling-runner-set-name": autoscalingListener.Spec.AutoscalingRunnerSetName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *resourceBuilder) newScaleSetListenerRole(autoscalingListener *v1alpha1.AutoscalingListener) *rbacv1.Role {
|
||||||
|
rules := rulesForListenerRole([]string{autoscalingListener.Spec.EphemeralRunnerSetName})
|
||||||
|
rulesHash := hash.ComputeTemplateHash(&rules)
|
||||||
|
newRole := &rbacv1.Role{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: scaleSetListenerRoleName(autoscalingListener),
|
||||||
|
Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
|
||||||
|
Labels: map[string]string{
|
||||||
|
"auto-scaling-runner-set-namespace": autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
|
||||||
|
"auto-scaling-runner-set-name": autoscalingListener.Spec.AutoscalingRunnerSetName,
|
||||||
|
"auto-scaling-listener-namespace": autoscalingListener.Namespace,
|
||||||
|
"auto-scaling-listener-name": autoscalingListener.Name,
|
||||||
|
"role-policy-rules-hash": rulesHash,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Rules: rules,
|
||||||
|
}
|
||||||
|
|
||||||
|
return newRole
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *resourceBuilder) newScaleSetListenerRoleBinding(autoscalingListener *v1alpha1.AutoscalingListener, listenerRole *rbacv1.Role, serviceAccount *corev1.ServiceAccount) *rbacv1.RoleBinding {
|
||||||
|
roleRef := rbacv1.RoleRef{
|
||||||
|
Kind: "Role",
|
||||||
|
Name: listenerRole.Name,
|
||||||
|
}
|
||||||
|
roleRefHash := hash.ComputeTemplateHash(&roleRef)
|
||||||
|
|
||||||
|
subjects := []rbacv1.Subject{
|
||||||
|
{
|
||||||
|
Kind: "ServiceAccount",
|
||||||
|
Namespace: serviceAccount.Namespace,
|
||||||
|
Name: serviceAccount.Name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
subjectHash := hash.ComputeTemplateHash(&subjects)
|
||||||
|
|
||||||
|
newRoleBinding := &rbacv1.RoleBinding{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: scaleSetListenerRoleName(autoscalingListener),
|
||||||
|
Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
|
||||||
|
Labels: map[string]string{
|
||||||
|
"auto-scaling-runner-set-namespace": autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
|
||||||
|
"auto-scaling-runner-set-name": autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
|
||||||
|
"auto-scaling-listener-namespace": autoscalingListener.Namespace,
|
||||||
|
"auto-scaling-listener-name": autoscalingListener.Name,
|
||||||
|
"role-binding-role-ref-hash": roleRefHash,
|
||||||
|
"role-binding-subject-hash": subjectHash,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RoleRef: roleRef,
|
||||||
|
Subjects: subjects,
|
||||||
|
}
|
||||||
|
|
||||||
|
return newRoleBinding
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *resourceBuilder) newScaleSetListenerSecretMirror(autoscalingListener *v1alpha1.AutoscalingListener, secret *corev1.Secret) *corev1.Secret {
|
||||||
|
dataHash := hash.ComputeTemplateHash(&secret.Data)
|
||||||
|
|
||||||
|
newListenerSecret := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: scaleSetListenerSecretMirrorName(autoscalingListener),
|
||||||
|
Namespace: autoscalingListener.Namespace,
|
||||||
|
Labels: map[string]string{
|
||||||
|
"auto-scaling-runner-set-namespace": autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
|
||||||
|
"auto-scaling-runner-set-name": autoscalingListener.Spec.AutoscalingRunnerSetName,
|
||||||
|
"secret-data-hash": dataHash,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Data: secret.DeepCopy().Data,
|
||||||
|
}
|
||||||
|
|
||||||
|
return newListenerSecret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *resourceBuilder) newAutoScalingListener(autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet, namespace, image string, imagePullSecrets []corev1.LocalObjectReference) (*v1alpha1.AutoscalingListener, error) {
|
||||||
|
runnerScaleSetId, err := strconv.Atoi(autoscalingRunnerSet.Annotations[runnerScaleSetIdKey])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
effectiveMinRunners := 0
|
||||||
|
effectiveMaxRunners := math.MaxInt32
|
||||||
|
if autoscalingRunnerSet.Spec.MaxRunners != nil {
|
||||||
|
effectiveMaxRunners = *autoscalingRunnerSet.Spec.MaxRunners
|
||||||
|
}
|
||||||
|
if autoscalingRunnerSet.Spec.MinRunners != nil {
|
||||||
|
effectiveMinRunners = *autoscalingRunnerSet.Spec.MinRunners
|
||||||
|
}
|
||||||
|
|
||||||
|
autoscalingListener := &v1alpha1.AutoscalingListener{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: scaleSetListenerName(autoscalingRunnerSet),
|
||||||
|
Namespace: namespace,
|
||||||
|
Labels: map[string]string{
|
||||||
|
"auto-scaling-runner-set-namespace": autoscalingRunnerSet.Namespace,
|
||||||
|
"auto-scaling-runner-set-name": autoscalingRunnerSet.Name,
|
||||||
|
LabelKeyRunnerSpecHash: autoscalingRunnerSet.ListenerSpecHash(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: v1alpha1.AutoscalingListenerSpec{
|
||||||
|
GitHubConfigUrl: autoscalingRunnerSet.Spec.GitHubConfigUrl,
|
||||||
|
GitHubConfigSecret: autoscalingRunnerSet.Spec.GitHubConfigSecret,
|
||||||
|
RunnerScaleSetId: runnerScaleSetId,
|
||||||
|
AutoscalingRunnerSetNamespace: autoscalingRunnerSet.Namespace,
|
||||||
|
AutoscalingRunnerSetName: autoscalingRunnerSet.Name,
|
||||||
|
EphemeralRunnerSetName: ephemeralRunnerSet.Name,
|
||||||
|
MinRunners: effectiveMinRunners,
|
||||||
|
MaxRunners: effectiveMaxRunners,
|
||||||
|
Image: image,
|
||||||
|
ImagePullSecrets: imagePullSecrets,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return autoscalingListener, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *resourceBuilder) newEphemeralRunner(ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet) *v1alpha1.EphemeralRunner {
|
||||||
|
return &v1alpha1.EphemeralRunner{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
GenerateName: ephemeralRunnerSet.Name + "-runner-",
|
||||||
|
Namespace: ephemeralRunnerSet.Namespace,
|
||||||
|
},
|
||||||
|
Spec: ephemeralRunnerSet.Spec.EphemeralRunnerSpec,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *resourceBuilder) newEphemeralRunnerPod(ctx context.Context, runner *v1alpha1.EphemeralRunner, secret *corev1.Secret) *corev1.Pod {
|
||||||
|
var newPod corev1.Pod
|
||||||
|
|
||||||
|
labels := map[string]string{}
|
||||||
|
annotations := map[string]string{}
|
||||||
|
|
||||||
|
for k, v := range runner.ObjectMeta.Labels {
|
||||||
|
labels[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range runner.Spec.PodTemplateSpec.Labels {
|
||||||
|
labels[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range runner.ObjectMeta.Annotations {
|
||||||
|
annotations[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range runner.Spec.PodTemplateSpec.Annotations {
|
||||||
|
annotations[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
labels[LabelKeyPodTemplateHash] = hash.FNVHashStringObjects(
|
||||||
|
FilterLabels(labels, LabelKeyRunnerTemplateHash),
|
||||||
|
annotations,
|
||||||
|
runner.Spec,
|
||||||
|
runner.Status.RunnerJITConfig,
|
||||||
|
)
|
||||||
|
|
||||||
|
labels["actions-ephemeral-runner"] = string(corev1.ConditionTrue)
|
||||||
|
|
||||||
|
objectMeta := metav1.ObjectMeta{
|
||||||
|
Name: runner.ObjectMeta.Name,
|
||||||
|
Namespace: runner.ObjectMeta.Namespace,
|
||||||
|
Labels: labels,
|
||||||
|
Annotations: annotations,
|
||||||
|
}
|
||||||
|
|
||||||
|
newPod.ObjectMeta = objectMeta
|
||||||
|
newPod.Spec = runner.Spec.PodTemplateSpec.Spec
|
||||||
|
newPod.Spec.Containers = make([]corev1.Container, 0, len(runner.Spec.PodTemplateSpec.Spec.Containers))
|
||||||
|
|
||||||
|
for _, c := range runner.Spec.PodTemplateSpec.Spec.Containers {
|
||||||
|
if c.Name == EphemeralRunnerContainerName {
|
||||||
|
c.Env = append(c.Env, corev1.EnvVar{
|
||||||
|
Name: EnvVarRunnerJITConfig,
|
||||||
|
ValueFrom: &corev1.EnvVarSource{
|
||||||
|
SecretKeyRef: &corev1.SecretKeySelector{
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{
|
||||||
|
Name: secret.Name,
|
||||||
|
},
|
||||||
|
Key: jitTokenKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
newPod.Spec.Containers = append(newPod.Spec.Containers, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &newPod
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *resourceBuilder) newEphemeralRunnerJitSecret(ephemeralRunner *v1alpha1.EphemeralRunner) *corev1.Secret {
|
||||||
|
return &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: ephemeralRunner.Name,
|
||||||
|
Namespace: ephemeralRunner.Namespace,
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
jitTokenKey: []byte(ephemeralRunner.Status.RunnerJITConfig),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scaleSetListenerName(autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet) string {
|
||||||
|
namespaceHash := hash.FNVHashString(autoscalingRunnerSet.Namespace)
|
||||||
|
if len(namespaceHash) > 8 {
|
||||||
|
namespaceHash = namespaceHash[:8]
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%v-%v-listener", autoscalingRunnerSet.Name, namespaceHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scaleSetListenerServiceAccountName(autoscalingListener *v1alpha1.AutoscalingListener) string {
|
||||||
|
namespaceHash := hash.FNVHashString(autoscalingListener.Spec.AutoscalingRunnerSetNamespace)
|
||||||
|
if len(namespaceHash) > 8 {
|
||||||
|
namespaceHash = namespaceHash[:8]
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%v-%v-listener", autoscalingListener.Spec.AutoscalingRunnerSetName, namespaceHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scaleSetListenerRoleName(autoscalingListener *v1alpha1.AutoscalingListener) string {
|
||||||
|
namespaceHash := hash.FNVHashString(autoscalingListener.Spec.AutoscalingRunnerSetNamespace)
|
||||||
|
if len(namespaceHash) > 8 {
|
||||||
|
namespaceHash = namespaceHash[:8]
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%v-%v-listener", autoscalingListener.Spec.AutoscalingRunnerSetName, namespaceHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scaleSetListenerSecretMirrorName(autoscalingListener *v1alpha1.AutoscalingListener) string {
|
||||||
|
namespaceHash := hash.FNVHashString(autoscalingListener.Spec.AutoscalingRunnerSetNamespace)
|
||||||
|
if len(namespaceHash) > 8 {
|
||||||
|
namespaceHash = namespaceHash[:8]
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%v-%v-listener", autoscalingListener.Spec.AutoscalingRunnerSetName, namespaceHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rulesForListenerRole(resourceNames []string) []rbacv1.PolicyRule {
|
||||||
|
return []rbacv1.PolicyRule{
|
||||||
|
{
|
||||||
|
APIGroups: []string{"actions.github.com"},
|
||||||
|
Resources: []string{"ephemeralrunnersets"},
|
||||||
|
ResourceNames: resourceNames,
|
||||||
|
Verbs: []string{"patch"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
APIGroups: []string{"actions.github.com"},
|
||||||
|
Resources: []string{"ephemeralrunners", "ephemeralrunners/status"},
|
||||||
|
Verbs: []string{"patch"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
91
controllers/actions.github.com/suite_test.go
Normal file
91
controllers/actions.github.com/suite_test.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 The actions-runner-controller authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package actionsgithubcom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/onsi/ginkgo/config"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
|
actionsv1alpha1 "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
|
||||||
|
"k8s.io/client-go/kubernetes/scheme"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
|
||||||
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||||
|
// +kubebuilder:scaffold:imports
|
||||||
|
)
|
||||||
|
|
||||||
|
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
|
||||||
|
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
|
||||||
|
|
||||||
|
var cfg *rest.Config
|
||||||
|
var k8sClient client.Client
|
||||||
|
var testEnv *envtest.Environment
|
||||||
|
|
||||||
|
func TestAPIs(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
|
||||||
|
config.GinkgoConfig.FocusStrings = append(config.GinkgoConfig.FocusStrings, os.Getenv("GINKGO_FOCUS"))
|
||||||
|
|
||||||
|
RunSpecsWithDefaultAndCustomReporters(t,
|
||||||
|
"Controller Suite",
|
||||||
|
[]Reporter{printer.NewlineReporter{}})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = BeforeSuite(func(done Done) {
|
||||||
|
logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter)))
|
||||||
|
|
||||||
|
By("bootstrapping test environment")
|
||||||
|
testEnv = &envtest.Environment{
|
||||||
|
CRDDirectoryPaths: []string{filepath.Join("../..", "config", "crd", "bases")},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoids the following error:
|
||||||
|
// 2021-03-19T15:14:11.673+0900 ERROR controller-runtime.controller Reconciler error {"controller": "testns-tvjzjrunner", "request": "testns-gdnyx/example-runnerdeploy-zps4z-j5562", "error": "Pod \"example-runnerdeploy-zps4z-j5562\" is invalid: [spec.containers[1].image: Required value, spec.containers[1].securityContext.privileged: Forbidden: disallowed by cluster policy]"}
|
||||||
|
testEnv.ControlPlane.GetAPIServer().Configure().
|
||||||
|
Append("allow-privileged", "true")
|
||||||
|
|
||||||
|
var err error
|
||||||
|
cfg, err = testEnv.Start()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(cfg).ToNot(BeNil())
|
||||||
|
|
||||||
|
err = actionsv1alpha1.AddToScheme(scheme.Scheme)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// +kubebuilder:scaffold:scheme
|
||||||
|
|
||||||
|
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(k8sClient).ToNot(BeNil())
|
||||||
|
|
||||||
|
close(done)
|
||||||
|
}, 60)
|
||||||
|
|
||||||
|
var _ = AfterSuite(func() {
|
||||||
|
By("tearing down the test environment")
|
||||||
|
err := testEnv.Stop()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
})
|
||||||
27
controllers/actions.github.com/utils.go
Normal file
27
controllers/actions.github.com/utils.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package actionsgithubcom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/util/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FilterLabels(labels map[string]string, filter string) map[string]string {
|
||||||
|
filtered := map[string]string{}
|
||||||
|
|
||||||
|
for k, v := range labels {
|
||||||
|
if k != filter {
|
||||||
|
filtered[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz1234567890")
|
||||||
|
|
||||||
|
func RandStringRunes(n int) string {
|
||||||
|
b := make([]rune, n)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
34
controllers/actions.github.com/utils_test.go
Normal file
34
controllers/actions.github.com/utils_test.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package actionsgithubcom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_filterLabels(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
labels map[string]string
|
||||||
|
filter string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ok",
|
||||||
|
args: args{
|
||||||
|
labels: map[string]string{LabelKeyRunnerTemplateHash: "abc", LabelKeyPodTemplateHash: "def"},
|
||||||
|
filter: LabelKeyRunnerTemplateHash,
|
||||||
|
},
|
||||||
|
want: map[string]string{LabelKeyPodTemplateHash: "def"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := FilterLabels(tt.args.labels, tt.args.filter); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("FilterLabels() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
1101
github/actions/client.go
Normal file
1101
github/actions/client.go
Normal file
File diff suppressed because it is too large
Load Diff
75
github/actions/client_generate_jit_test.go
Normal file
75
github/actions/client_generate_jit_test.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package actions_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/hashicorp/go-retryablehttp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateJitRunnerConfig(t *testing.T) {
|
||||||
|
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
|
||||||
|
|
||||||
|
t.Run("Get JIT Config for Runner", func(t *testing.T) {
|
||||||
|
name := "Get JIT Config for Runner"
|
||||||
|
want := &actions.RunnerScaleSetJitRunnerConfig{}
|
||||||
|
response := []byte(`{"count":1,"value":[{"id":1,"name":"scale-set-name"}]}`)
|
||||||
|
|
||||||
|
runnerSettings := &actions.RunnerScaleSetJitRunnerSetting{}
|
||||||
|
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(response)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := actionsClient.GenerateJitRunnerConfig(context.Background(), runnerSettings, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateJitRunnerConfig got unexepected error, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("GenerateJitRunnerConfig(%v) mismatch (-want +got):\n%s", name, diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
runnerSettings := &actions.RunnerScaleSetJitRunnerSetting{}
|
||||||
|
|
||||||
|
retryClient := retryablehttp.NewClient()
|
||||||
|
retryClient.RetryWaitMax = 1 * time.Millisecond
|
||||||
|
retryClient.RetryMax = 1
|
||||||
|
|
||||||
|
actualRetry := 0
|
||||||
|
expectedRetry := retryClient.RetryMax + 1
|
||||||
|
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
httpClient := retryClient.StandardClient()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
Client: httpClient,
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = actionsClient.GenerateJitRunnerConfig(context.Background(), runnerSettings, 1)
|
||||||
|
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
})
|
||||||
|
}
|
||||||
144
github/actions/client_job_acquisition_test.go
Normal file
144
github/actions/client_job_acquisition_test.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package actions_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/hashicorp/go-retryablehttp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAcquireJobs(t *testing.T) {
|
||||||
|
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
|
||||||
|
|
||||||
|
t.Run("Acquire Job", func(t *testing.T) {
|
||||||
|
name := "Acquire Job"
|
||||||
|
|
||||||
|
want := []int64{1}
|
||||||
|
response := []byte(`{"value": [1]}`)
|
||||||
|
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{Id: 1},
|
||||||
|
MessageQueueAccessToken: "abc",
|
||||||
|
}
|
||||||
|
requestIDs := want
|
||||||
|
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(response)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := actionsClient.AcquireJobs(context.Background(), session.RunnerScaleSet.Id, session.MessageQueueAccessToken, requestIDs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateRunnerScaleSet got unexepected error, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("GetRunnerScaleSet(%v) mismatch (-want +got):\n%s", name, diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{Id: 1},
|
||||||
|
MessageQueueAccessToken: "abc",
|
||||||
|
}
|
||||||
|
var requestIDs []int64 = []int64{1}
|
||||||
|
|
||||||
|
retryClient := retryablehttp.NewClient()
|
||||||
|
retryClient.RetryWaitMax = 1 * time.Millisecond
|
||||||
|
retryClient.RetryMax = 1
|
||||||
|
|
||||||
|
actualRetry := 0
|
||||||
|
expectedRetry := retryClient.RetryMax + 1
|
||||||
|
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
httpClient := retryClient.StandardClient()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
Client: httpClient,
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = actionsClient.AcquireJobs(context.Background(), session.RunnerScaleSet.Id, session.MessageQueueAccessToken, requestIDs)
|
||||||
|
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAcquirableJobs(t *testing.T) {
|
||||||
|
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
|
||||||
|
|
||||||
|
t.Run("Acquire Job", func(t *testing.T) {
|
||||||
|
name := "Acquire Job"
|
||||||
|
|
||||||
|
want := &actions.AcquirableJobList{}
|
||||||
|
response := []byte(`{"count": 0}`)
|
||||||
|
|
||||||
|
runnerScaleSet := &actions.RunnerScaleSet{Id: 1}
|
||||||
|
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(response)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := actionsClient.GetAcquirableJobs(context.Background(), runnerScaleSet.Id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetAcquirableJobs got unexepected error, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("GetAcquirableJobs(%v) mismatch (-want +got):\n%s", name, diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
runnerScaleSet := &actions.RunnerScaleSet{Id: 1}
|
||||||
|
|
||||||
|
retryClient := retryablehttp.NewClient()
|
||||||
|
retryClient.RetryWaitMax = 1 * time.Millisecond
|
||||||
|
retryClient.RetryMax = 1
|
||||||
|
|
||||||
|
actualRetry := 0
|
||||||
|
expectedRetry := retryClient.RetryMax + 1
|
||||||
|
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
httpClient := retryClient.StandardClient()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
Client: httpClient,
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = actionsClient.GetAcquirableJobs(context.Background(), runnerScaleSet.Id)
|
||||||
|
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
})
|
||||||
|
}
|
||||||
269
github/actions/client_runner_scale_set_message_test.go
Normal file
269
github/actions/client_runner_scale_set_message_test.go
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
package actions_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/hashicorp/go-retryablehttp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetMessage(t *testing.T) {
|
||||||
|
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
|
||||||
|
runnerScaleSetMessage := &actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "rssType",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Get Runner Scale Set Message", func(t *testing.T) {
|
||||||
|
want := runnerScaleSetMessage
|
||||||
|
response := []byte(`{"messageId":1,"messageType":"rssType"}`)
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(response)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := actionsClient.GetMessage(context.Background(), s.URL, token, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetMessage got unexepected error, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("GetMessage mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
retryClient := retryablehttp.NewClient()
|
||||||
|
retryClient.RetryWaitMax = 1 * time.Nanosecond
|
||||||
|
retryClient.RetryMax = 1
|
||||||
|
|
||||||
|
actualRetry := 0
|
||||||
|
expectedRetry := retryClient.RetryMax + 1
|
||||||
|
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
httpClient := retryClient.StandardClient()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
Client: httpClient,
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = actionsClient.GetMessage(context.Background(), s.URL, token, 0)
|
||||||
|
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Custom retries on server error", func(t *testing.T) {
|
||||||
|
actualRetry := 0
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
retryMax := 1
|
||||||
|
retryWaitMax := 1 * time.Nanosecond
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
RetryMax: &retryMax,
|
||||||
|
RetryWaitMax: &retryWaitMax,
|
||||||
|
}
|
||||||
|
_, _ = actionsClient.GetMessage(context.Background(), s.URL, token, 0)
|
||||||
|
expectedRetry := retryMax + 1
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Message token expired", func(t *testing.T) {
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
_, err := actionsClient.GetMessage(context.Background(), s.URL, token, 0)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("GetMessage did not get exepected error, ")
|
||||||
|
}
|
||||||
|
var expectedErr *actions.MessageQueueTokenExpiredError
|
||||||
|
require.True(t, errors.As(err, &expectedErr))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Status code not found", func(t *testing.T) {
|
||||||
|
want := actions.ActionsError{
|
||||||
|
Message: "Request returned status: 404 Not Found",
|
||||||
|
StatusCode: 404,
|
||||||
|
}
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
_, err := actionsClient.GetMessage(context.Background(), s.URL, token, 0)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("GetMessage did not get exepected error, ")
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(want.Error(), err.Error()); diff != "" {
|
||||||
|
t.Errorf("GetMessage mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
_, err := actionsClient.GetMessage(context.Background(), s.URL, token, 0)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("GetMessage did not get exepected error,")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteMessage(t *testing.T) {
|
||||||
|
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
|
||||||
|
runnerScaleSetMessage := &actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "rssType",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Delete existing message", func(t *testing.T) {
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
err := actionsClient.DeleteMessage(context.Background(), s.URL, token, runnerScaleSetMessage.MessageId)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeleteMessage got unexepected error, %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Message token expired", func(t *testing.T) {
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
err := actionsClient.DeleteMessage(context.Background(), s.URL, token, 0)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("DeleteMessage did not get exepected error, ")
|
||||||
|
}
|
||||||
|
var expectedErr *actions.MessageQueueTokenExpiredError
|
||||||
|
require.True(t, errors.As(err, &expectedErr))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
err := actionsClient.DeleteMessage(context.Background(), s.URL, token, runnerScaleSetMessage.MessageId)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("DeleteMessage did not get exepected error")
|
||||||
|
}
|
||||||
|
var expectedErr *actions.ActionsError
|
||||||
|
require.True(t, errors.As(err, &expectedErr))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
actualRetry := 0
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
retryClient := retryablehttp.NewClient()
|
||||||
|
retryMax := 1
|
||||||
|
retryClient.RetryWaitMax = time.Nanosecond
|
||||||
|
retryClient.RetryMax = retryMax
|
||||||
|
httpClient := retryClient.StandardClient()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
Client: httpClient,
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
_ = actionsClient.DeleteMessage(context.Background(), s.URL, token, runnerScaleSetMessage.MessageId)
|
||||||
|
expectedRetry := retryMax + 1
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("No message found", func(t *testing.T) {
|
||||||
|
want := (*actions.RunnerScaleSetMessage)(nil)
|
||||||
|
rsl, err := json.Marshal(want)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(rsl)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
err = actionsClient.DeleteMessage(context.Background(), s.URL, token, runnerScaleSetMessage.MessageId+1)
|
||||||
|
var expectedErr *actions.ActionsError
|
||||||
|
require.True(t, errors.As(err, &expectedErr))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
244
github/actions/client_runner_scale_set_session_test.go
Normal file
244
github/actions/client_runner_scale_set_session_test.go
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
package actions_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateMessageSession(t *testing.T) {
|
||||||
|
t.Run("CreateMessageSession unmarshals correctly", func(t *testing.T) {
|
||||||
|
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
|
||||||
|
owner := "foo"
|
||||||
|
runnerScaleSet := actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
Name: "ScaleSet",
|
||||||
|
CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
RunnerSetting: actions.RunnerSetting{},
|
||||||
|
}
|
||||||
|
|
||||||
|
want := &actions.RunnerScaleSetSession{
|
||||||
|
OwnerName: "foo",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
Name: "ScaleSet",
|
||||||
|
},
|
||||||
|
MessageQueueUrl: "http://fake.actions.github.com/123",
|
||||||
|
MessageQueueAccessToken: "fake.jwt.here",
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
resp := []byte(`{
|
||||||
|
"ownerName": "foo",
|
||||||
|
"runnerScaleSet": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "ScaleSet"
|
||||||
|
},
|
||||||
|
"messageQueueUrl": "http://fake.actions.github.com/123",
|
||||||
|
"messageQueueAccessToken": "fake.jwt.here"
|
||||||
|
}`)
|
||||||
|
w.Write(resp)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
retryMax := 1
|
||||||
|
retryWaitMax := 1 * time.Microsecond
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &srv.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
RetryMax: &retryMax,
|
||||||
|
RetryWaitMax: &retryWaitMax,
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := actionsClient.CreateMessageSession(context.Background(), runnerScaleSet.Id, owner)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateMessageSession got unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(got, want); diff != "" {
|
||||||
|
t.Fatalf("CreateMessageSession got unexpected diff: -want +got: %v", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CreateMessageSession unmarshals errors into ActionsError", func(t *testing.T) {
|
||||||
|
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
|
||||||
|
owner := "foo"
|
||||||
|
runnerScaleSet := actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
Name: "ScaleSet",
|
||||||
|
CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
RunnerSetting: actions.RunnerSetting{},
|
||||||
|
}
|
||||||
|
|
||||||
|
want := &actions.ActionsError{
|
||||||
|
ExceptionName: "CSharpExceptionNameHere",
|
||||||
|
Message: "could not do something",
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
resp := []byte(`{"typeName": "CSharpExceptionNameHere","message": "could not do something"}`)
|
||||||
|
w.Write(resp)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
retryMax := 1
|
||||||
|
retryWaitMax := 1 * time.Microsecond
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &srv.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
RetryMax: &retryMax,
|
||||||
|
RetryWaitMax: &retryWaitMax,
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := actionsClient.CreateMessageSession(context.Background(), runnerScaleSet.Id, owner)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("CreateMessageSession did not get expected error: %v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
errorTypeForComparison := &actions.ActionsError{}
|
||||||
|
if isActionsError := errors.As(err, &errorTypeForComparison); !isActionsError {
|
||||||
|
t.Fatalf("CreateMessageSession expected to be able to parse the error into ActionsError type: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotErr := err.(*actions.ActionsError)
|
||||||
|
|
||||||
|
if diff := cmp.Diff(want, gotErr); diff != "" {
|
||||||
|
t.Fatalf("CreateMessageSession got unexpected diff: -want +got: %v", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CreateMessageSession call is retried the correct amount of times", func(t *testing.T) {
|
||||||
|
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
|
||||||
|
owner := "foo"
|
||||||
|
runnerScaleSet := actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
Name: "ScaleSet",
|
||||||
|
CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
RunnerSetting: actions.RunnerSetting{},
|
||||||
|
}
|
||||||
|
|
||||||
|
gotRetries := 0
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
gotRetries++
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
retryMax := 3
|
||||||
|
retryWaitMax, err := time.ParseDuration("1µs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantRetries := retryMax + 1
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &srv.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
RetryMax: &retryMax,
|
||||||
|
RetryWaitMax: &retryWaitMax,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = actionsClient.CreateMessageSession(context.Background(), runnerScaleSet.Id, owner)
|
||||||
|
|
||||||
|
assert.Equalf(t, gotRetries, wantRetries, "CreateMessageSession got unexpected retry count: got=%v, want=%v", gotRetries, wantRetries)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteMessageSession(t *testing.T) {
|
||||||
|
t.Run("DeleteMessageSession call is retried the correct amount of times", func(t *testing.T) {
|
||||||
|
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
|
||||||
|
runnerScaleSet := actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
Name: "ScaleSet",
|
||||||
|
CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
RunnerSetting: actions.RunnerSetting{},
|
||||||
|
}
|
||||||
|
|
||||||
|
gotRetries := 0
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
gotRetries++
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
retryMax := 3
|
||||||
|
retryWaitMax, err := time.ParseDuration("1µs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantRetries := retryMax + 1
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &srv.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
RetryMax: &retryMax,
|
||||||
|
RetryWaitMax: &retryWaitMax,
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionId := uuid.New()
|
||||||
|
|
||||||
|
_ = actionsClient.DeleteMessageSession(context.Background(), runnerScaleSet.Id, &sessionId)
|
||||||
|
|
||||||
|
assert.Equalf(t, gotRetries, wantRetries, "CreateMessageSession got unexpected retry count: got=%v, want=%v", gotRetries, wantRetries)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefreshMessageSession(t *testing.T) {
|
||||||
|
t.Run("RefreshMessageSession call is retried the correct amount of times", func(t *testing.T) {
|
||||||
|
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
|
||||||
|
runnerScaleSet := actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
Name: "ScaleSet",
|
||||||
|
CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
RunnerSetting: actions.RunnerSetting{},
|
||||||
|
}
|
||||||
|
|
||||||
|
gotRetries := 0
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
gotRetries++
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
retryMax := 3
|
||||||
|
retryWaitMax, err := time.ParseDuration("1µs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantRetries := retryMax + 1
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &srv.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
RetryMax: &retryMax,
|
||||||
|
RetryWaitMax: &retryWaitMax,
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionId := uuid.New()
|
||||||
|
|
||||||
|
_, _ = actionsClient.RefreshMessageSession(context.Background(), runnerScaleSet.Id, &sessionId)
|
||||||
|
|
||||||
|
assert.Equalf(t, gotRetries, wantRetries, "CreateMessageSession got unexpected retry count: got=%v, want=%v", gotRetries, wantRetries)
|
||||||
|
})
|
||||||
|
}
|
||||||
858
github/actions/client_runner_scale_set_test.go
Normal file
858
github/actions/client_runner_scale_set_test.go
Normal file
@@ -0,0 +1,858 @@
|
|||||||
|
package actions_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/hashicorp/go-retryablehttp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetRunnerScaleSet(t *testing.T) {
|
||||||
|
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
|
||||||
|
scaleSetName := "ScaleSet"
|
||||||
|
runnerScaleSet := actions.RunnerScaleSet{Id: 1, Name: scaleSetName}
|
||||||
|
|
||||||
|
t.Run("Get existing scale set", func(t *testing.T) {
|
||||||
|
want := &runnerScaleSet
|
||||||
|
runnerScaleSetsResp := []byte(`{"count":1,"value":[{"id":1,"name":"ScaleSet"}]}`)
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(runnerScaleSetsResp)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
got, err := actionsClient.GetRunnerScaleSet(context.Background(), scaleSetName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateRunnerScaleSet got unexepected error, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("GetRunnerScaleSet(%v) mismatch (-want +got):\n%s", scaleSetName, diff)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("GetRunnerScaleSet calls correct url", func(t *testing.T) {
|
||||||
|
runnerScaleSetsResp := []byte(`{"count":1,"value":[{"id":1,"name":"ScaleSet"}]}`)
|
||||||
|
url := url.URL{}
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(runnerScaleSetsResp)
|
||||||
|
url = *r.URL
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
_, err := actionsClient.GetRunnerScaleSet(context.Background(), scaleSetName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateRunnerScaleSet got unexepected error, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u := url.String()
|
||||||
|
expectedUrl := fmt.Sprintf("/_apis/runtime/runnerscalesets?name=%s&api-version=6.0-preview", scaleSetName)
|
||||||
|
assert.Equal(t, expectedUrl, u)
|
||||||
|
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Status code not found", func(t *testing.T) {
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
_, err := actionsClient.GetRunnerScaleSet(context.Background(), scaleSetName)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("GetRunnerScaleSet did not get exepected error, ")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
_, err := actionsClient.GetRunnerScaleSet(context.Background(), scaleSetName)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("GetRunnerScaleSet did not get exepected error,")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
actualRetry := 0
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
retryClient := retryablehttp.NewClient()
|
||||||
|
retryMax := 1
|
||||||
|
retryWaitMax, err := time.ParseDuration("1µs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
retryClient.RetryWaitMax = retryWaitMax
|
||||||
|
retryClient.RetryMax = retryMax
|
||||||
|
httpClient := retryClient.StandardClient()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
Client: httpClient,
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
_, _ = actionsClient.GetRunnerScaleSet(context.Background(), scaleSetName)
|
||||||
|
expectedRetry := retryMax + 1
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Custom retries on server error", func(t *testing.T) {
|
||||||
|
actualRetry := 0
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
retryMax := 1
|
||||||
|
retryWaitMax, err := time.ParseDuration("1µs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
RetryMax: &retryMax,
|
||||||
|
RetryWaitMax: &retryWaitMax,
|
||||||
|
}
|
||||||
|
_, _ = actionsClient.GetRunnerScaleSet(context.Background(), scaleSetName)
|
||||||
|
expectedRetry := retryMax + 1
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("RunnerScaleSet count is zero", func(t *testing.T) {
|
||||||
|
want := (*actions.RunnerScaleSet)(nil)
|
||||||
|
runnerScaleSetsResp := []byte(`{"count":0,"value":[{"id":1,"name":"ScaleSet"}]}`)
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(runnerScaleSetsResp)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
got, _ := actionsClient.GetRunnerScaleSet(context.Background(), scaleSetName)
|
||||||
|
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("GetRunnerScaleSet(%v) mismatch (-want +got):\n%s", scaleSetName, diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Multiple runner scale sets found", func(t *testing.T) {
|
||||||
|
wantErr := fmt.Errorf("multiple runner scale sets found with name %s", scaleSetName)
|
||||||
|
runnerScaleSetsResp := []byte(`{"count":2,"value":[{"id":1,"name":"ScaleSet"}]}`)
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(runnerScaleSetsResp)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
_, err := actionsClient.GetRunnerScaleSet(context.Background(), scaleSetName)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("GetRunnerScaleSet did not get exepected error, %v", wantErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(wantErr.Error(), err.Error()); diff != "" {
|
||||||
|
t.Errorf("GetRunnerScaleSet(%v) mismatch (-want +got):\n%s", scaleSetName, diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRunnerScaleSetById(t *testing.T) {
|
||||||
|
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
|
||||||
|
scaleSetCreationDateTime := time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
runnerScaleSet := actions.RunnerScaleSet{Id: 1, Name: "ScaleSet", CreatedOn: scaleSetCreationDateTime, RunnerSetting: actions.RunnerSetting{}}
|
||||||
|
|
||||||
|
t.Run("Get existing scale set by Id", func(t *testing.T) {
|
||||||
|
want := &runnerScaleSet
|
||||||
|
rsl, err := json.Marshal(want)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(rsl)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
got, err := actionsClient.GetRunnerScaleSetById(context.Background(), runnerScaleSet.Id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetRunnerScaleSetById got unexepected error, %v", err)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("GetRunnerScaleSetById(%d) mismatch (-want +got):\n%s", runnerScaleSet.Id, diff)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("GetRunnerScaleSetById calls correct url", func(t *testing.T) {
|
||||||
|
rsl, err := json.Marshal(&runnerScaleSet)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
url := url.URL{}
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(rsl)
|
||||||
|
url = *r.URL
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
_, err = actionsClient.GetRunnerScaleSetById(context.Background(), runnerScaleSet.Id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetRunnerScaleSetById got unexepected error, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u := url.String()
|
||||||
|
expectedUrl := fmt.Sprintf("/_apis/runtime/runnerscalesets/%d?api-version=6.0-preview", runnerScaleSet.Id)
|
||||||
|
assert.Equal(t, expectedUrl, u)
|
||||||
|
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Status code not found", func(t *testing.T) {
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
_, err := actionsClient.GetRunnerScaleSetById(context.Background(), runnerScaleSet.Id)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("GetRunnerScaleSetById did not get exepected error, ")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
_, err := actionsClient.GetRunnerScaleSetById(context.Background(), runnerScaleSet.Id)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("GetRunnerScaleSetById did not get exepected error,")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
actualRetry := 0
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
retryClient := retryablehttp.NewClient()
|
||||||
|
retryMax := 1
|
||||||
|
retryWaitMax, err := time.ParseDuration("1µs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
retryClient.RetryWaitMax = retryWaitMax
|
||||||
|
retryClient.RetryMax = retryMax
|
||||||
|
httpClient := retryClient.StandardClient()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
Client: httpClient,
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
_, _ = actionsClient.GetRunnerScaleSetById(context.Background(), runnerScaleSet.Id)
|
||||||
|
expectedRetry := retryMax + 1
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Custom retries on server error", func(t *testing.T) {
|
||||||
|
actualRetry := 0
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
retryMax := 1
|
||||||
|
retryWaitMax, err := time.ParseDuration("1µs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
RetryMax: &retryMax,
|
||||||
|
RetryWaitMax: &retryWaitMax,
|
||||||
|
}
|
||||||
|
_, _ = actionsClient.GetRunnerScaleSetById(context.Background(), runnerScaleSet.Id)
|
||||||
|
expectedRetry := retryMax + 1
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("No RunnerScaleSet found", func(t *testing.T) {
|
||||||
|
want := (*actions.RunnerScaleSet)(nil)
|
||||||
|
rsl, err := json.Marshal(want)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(rsl)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
got, _ := actionsClient.GetRunnerScaleSetById(context.Background(), runnerScaleSet.Id)
|
||||||
|
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("GetRunnerScaleSetById(%v) mismatch (-want +got):\n%s", runnerScaleSet.Id, diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateRunnerScaleSet(t *testing.T) {
|
||||||
|
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
|
||||||
|
scaleSetCreationDateTime := time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
runnerScaleSet := actions.RunnerScaleSet{Id: 1, Name: "ScaleSet", CreatedOn: scaleSetCreationDateTime, RunnerSetting: actions.RunnerSetting{}}
|
||||||
|
|
||||||
|
t.Run("Create runner scale set", func(t *testing.T) {
|
||||||
|
want := &runnerScaleSet
|
||||||
|
rsl, err := json.Marshal(want)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(rsl)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
got, err := actionsClient.CreateRunnerScaleSet(context.Background(), &runnerScaleSet)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateRunnerScaleSet got exepected error, %v", err)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("CreateRunnerScaleSet(%d) mismatch (-want +got):\n%s", runnerScaleSet.Id, diff)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("CreateRunnerScaleSet calls correct url", func(t *testing.T) {
|
||||||
|
rsl, err := json.Marshal(&runnerScaleSet)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
url := url.URL{}
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(rsl)
|
||||||
|
url = *r.URL
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
_, err = actionsClient.CreateRunnerScaleSet(context.Background(), &runnerScaleSet)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateRunnerScaleSet got unexepected error, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u := url.String()
|
||||||
|
expectedUrl := "/_apis/runtime/runnerscalesets?api-version=6.0-preview"
|
||||||
|
assert.Equal(t, expectedUrl, u)
|
||||||
|
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
_, err := actionsClient.CreateRunnerScaleSet(context.Background(), &runnerScaleSet)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("CreateRunnerScaleSet did not get exepected error, %v", &actions.ActionsError{})
|
||||||
|
}
|
||||||
|
var expectedErr *actions.ActionsError
|
||||||
|
require.True(t, errors.As(err, &expectedErr))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
actualRetry := 0
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
retryClient := retryablehttp.NewClient()
|
||||||
|
retryMax := 1
|
||||||
|
retryWaitMax, err := time.ParseDuration("1µs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
retryClient.RetryMax = retryMax
|
||||||
|
retryClient.RetryWaitMax = retryWaitMax
|
||||||
|
|
||||||
|
httpClient := retryClient.StandardClient()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
Client: httpClient,
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
_, _ = actionsClient.CreateRunnerScaleSet(context.Background(), &runnerScaleSet)
|
||||||
|
expectedRetry := retryMax + 1
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Custom retries on server error", func(t *testing.T) {
|
||||||
|
actualRetry := 0
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
retryMax := 1
|
||||||
|
retryWaitMax, err := time.ParseDuration("1µs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
RetryMax: &retryMax,
|
||||||
|
RetryWaitMax: &retryWaitMax,
|
||||||
|
}
|
||||||
|
_, _ = actionsClient.CreateRunnerScaleSet(context.Background(), &runnerScaleSet)
|
||||||
|
expectedRetry := retryMax + 1
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateRunnerScaleSet(t *testing.T) {
|
||||||
|
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
|
||||||
|
scaleSetCreationDateTime := time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
runnerScaleSet := actions.RunnerScaleSet{Id: 1, Name: "ScaleSet", CreatedOn: scaleSetCreationDateTime, RunnerSetting: actions.RunnerSetting{}}
|
||||||
|
|
||||||
|
t.Run("Update existing scale set", func(t *testing.T) {
|
||||||
|
want := &runnerScaleSet
|
||||||
|
rsl, err := json.Marshal(want)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(rsl)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
got, err := actionsClient.UpdateRunnerScaleSet(context.Background(), runnerScaleSet.Id, want)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("UpdateRunnerScaleSet got exepected error, %v", err)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("UpdateRunnerScaleSet(%d) mismatch (-want +got):\n%s", runnerScaleSet.Id, diff)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("UpdateRunnerScaleSet calls correct url", func(t *testing.T) {
|
||||||
|
rsl, err := json.Marshal(&runnerScaleSet)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
url := url.URL{}
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(rsl)
|
||||||
|
url = *r.URL
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
_, err = actionsClient.UpdateRunnerScaleSet(context.Background(), runnerScaleSet.Id, &runnerScaleSet)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("UpdateRunnerScaleSet got unexepected error, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u := url.String()
|
||||||
|
expectedUrl := fmt.Sprintf("/_apis/runtime/runnerscalesets/%d?api-version=6.0-preview", runnerScaleSet.Id)
|
||||||
|
assert.Equal(t, expectedUrl, u)
|
||||||
|
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Status code not found", func(t *testing.T) {
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
_, err := actionsClient.UpdateRunnerScaleSet(context.Background(), runnerScaleSet.Id, &runnerScaleSet)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("UpdateRunnerScaleSet did not get exepected error,")
|
||||||
|
}
|
||||||
|
var expectedErr *actions.ActionsError
|
||||||
|
require.True(t, errors.As(err, &expectedErr))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
_, err := actionsClient.UpdateRunnerScaleSet(context.Background(), runnerScaleSet.Id, &runnerScaleSet)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("UpdateRunnerScaleSet did not get exepected error")
|
||||||
|
}
|
||||||
|
var expectedErr *actions.ActionsError
|
||||||
|
require.True(t, errors.As(err, &expectedErr))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
actualRetry := 0
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
retryClient := retryablehttp.NewClient()
|
||||||
|
retryMax := 1
|
||||||
|
retryWaitMax, err := time.ParseDuration("1µs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
retryClient.RetryWaitMax = retryWaitMax
|
||||||
|
retryClient.RetryMax = retryMax
|
||||||
|
httpClient := retryClient.StandardClient()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
Client: httpClient,
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
_, _ = actionsClient.UpdateRunnerScaleSet(context.Background(), runnerScaleSet.Id, &runnerScaleSet)
|
||||||
|
expectedRetry := retryMax + 1
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Custom retries on server error", func(t *testing.T) {
|
||||||
|
actualRetry := 0
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
retryMax := 1
|
||||||
|
retryWaitMax, err := time.ParseDuration("1µs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
RetryMax: &retryMax,
|
||||||
|
RetryWaitMax: &retryWaitMax,
|
||||||
|
}
|
||||||
|
_, _ = actionsClient.UpdateRunnerScaleSet(context.Background(), runnerScaleSet.Id, &runnerScaleSet)
|
||||||
|
expectedRetry := retryMax + 1
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("No RunnerScaleSet found", func(t *testing.T) {
|
||||||
|
want := (*actions.RunnerScaleSet)(nil)
|
||||||
|
rsl, err := json.Marshal(want)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(rsl)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
got, err := actionsClient.UpdateRunnerScaleSet(context.Background(), runnerScaleSet.Id, &runnerScaleSet)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("UpdateRunnerScaleSet got unexepected error, %v", err)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("UpdateRunnerScaleSet(%v) mismatch (-want +got):\n%s", runnerScaleSet.Id, diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteRunnerScaleSet(t *testing.T) {
|
||||||
|
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
|
||||||
|
scaleSetCreationDateTime := time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
runnerScaleSet := actions.RunnerScaleSet{Id: 1, Name: "ScaleSet", CreatedOn: scaleSetCreationDateTime, RunnerSetting: actions.RunnerSetting{}}
|
||||||
|
|
||||||
|
t.Run("Delete existing scale set", func(t *testing.T) {
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
err := actionsClient.DeleteRunnerScaleSet(context.Background(), runnerScaleSet.Id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeleteRunnerScaleSet got unexepected error, %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("DeleteRunnerScaleSet calls correct url", func(t *testing.T) {
|
||||||
|
url := url.URL{}
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
url = *r.URL
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
err := actionsClient.DeleteRunnerScaleSet(context.Background(), runnerScaleSet.Id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeleteRunnerScaleSet got unexepected error, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u := url.String()
|
||||||
|
expectedUrl := fmt.Sprintf("/_apis/runtime/runnerscalesets/%d?api-version=6.0-preview", runnerScaleSet.Id)
|
||||||
|
assert.Equal(t, expectedUrl, u)
|
||||||
|
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Status code not found", func(t *testing.T) {
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
err := actionsClient.DeleteRunnerScaleSet(context.Background(), runnerScaleSet.Id)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("DeleteRunnerScaleSet did not get exepected error, ")
|
||||||
|
}
|
||||||
|
var expectedErr *actions.ActionsError
|
||||||
|
require.True(t, errors.As(err, &expectedErr))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
err := actionsClient.DeleteRunnerScaleSet(context.Background(), runnerScaleSet.Id)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("DeleteRunnerScaleSet did not get exepected error")
|
||||||
|
}
|
||||||
|
var expectedErr *actions.ActionsError
|
||||||
|
require.True(t, errors.As(err, &expectedErr))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
actualRetry := 0
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
retryClient := retryablehttp.NewClient()
|
||||||
|
retryMax := 1
|
||||||
|
retryWaitMax, err := time.ParseDuration("1µs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
retryClient.RetryWaitMax = retryWaitMax
|
||||||
|
retryClient.RetryMax = retryMax
|
||||||
|
httpClient := retryClient.StandardClient()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
Client: httpClient,
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
_ = actionsClient.DeleteRunnerScaleSet(context.Background(), runnerScaleSet.Id)
|
||||||
|
expectedRetry := retryMax + 1
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Custom retries on server error", func(t *testing.T) {
|
||||||
|
actualRetry := 0
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
retryMax := 1
|
||||||
|
retryWaitMax, err := time.ParseDuration("1µs")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
RetryMax: &retryMax,
|
||||||
|
RetryWaitMax: &retryWaitMax,
|
||||||
|
}
|
||||||
|
_ = actionsClient.DeleteRunnerScaleSet(context.Background(), runnerScaleSet.Id)
|
||||||
|
expectedRetry := retryMax + 1
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("No RunnerScaleSet found", func(t *testing.T) {
|
||||||
|
want := (*actions.RunnerScaleSet)(nil)
|
||||||
|
rsl, err := json.Marshal(want)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(rsl)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
err = actionsClient.DeleteRunnerScaleSet(context.Background(), runnerScaleSet.Id)
|
||||||
|
var expectedErr *actions.ActionsError
|
||||||
|
require.True(t, errors.As(err, &expectedErr))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
219
github/actions/client_runner_test.go
Normal file
219
github/actions/client_runner_test.go
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
package actions_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/hashicorp/go-retryablehttp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tokenExpireAt = time.Now().Add(10 * time.Minute)
|
||||||
|
|
||||||
|
func TestGetRunner(t *testing.T) {
|
||||||
|
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
|
||||||
|
|
||||||
|
t.Run("Get Runner", func(t *testing.T) {
|
||||||
|
name := "Get Runner"
|
||||||
|
var runnerID int64 = 1
|
||||||
|
want := &actions.RunnerReference{
|
||||||
|
Id: int(runnerID),
|
||||||
|
Name: "self-hosted-ubuntu",
|
||||||
|
}
|
||||||
|
response := []byte(`{"id": 1, "name": "self-hosted-ubuntu"}`)
|
||||||
|
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(response)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := actionsClient.GetRunner(context.Background(), runnerID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetRunner got unexepected error, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("GetRunner(%v) mismatch (-want +got):\n%s", name, diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
var runnerID int64 = 1
|
||||||
|
retryClient := retryablehttp.NewClient()
|
||||||
|
retryClient.RetryWaitMax = 1 * time.Millisecond
|
||||||
|
retryClient.RetryMax = 1
|
||||||
|
|
||||||
|
actualRetry := 0
|
||||||
|
expectedRetry := retryClient.RetryMax + 1
|
||||||
|
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
httpClient := retryClient.StandardClient()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
Client: httpClient,
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = actionsClient.GetRunner(context.Background(), runnerID)
|
||||||
|
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRunnerByName(t *testing.T) {
|
||||||
|
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
|
||||||
|
|
||||||
|
t.Run("Get Runner by Name", func(t *testing.T) {
|
||||||
|
var runnerID int64 = 1
|
||||||
|
var runnerName string = "self-hosted-ubuntu"
|
||||||
|
want := &actions.RunnerReference{
|
||||||
|
Id: int(runnerID),
|
||||||
|
Name: runnerName,
|
||||||
|
}
|
||||||
|
response := []byte(`{"count": 1, "value": [{"id": 1, "name": "self-hosted-ubuntu"}]}`)
|
||||||
|
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(response)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := actionsClient.GetRunnerByName(context.Background(), runnerName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetRunnerByName got unexepected error, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("GetRunnerByName(%v) mismatch (-want +got):\n%s", runnerName, diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Get Runner by name with not exist runner", func(t *testing.T) {
|
||||||
|
var runnerName string = "self-hosted-ubuntu"
|
||||||
|
response := []byte(`{"count": 0, "value": []}`)
|
||||||
|
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(response)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := actionsClient.GetRunnerByName(context.Background(), runnerName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetRunnerByName got unexepected error, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff((*actions.RunnerReference)(nil), got); diff != "" {
|
||||||
|
t.Errorf("GetRunnerByName(%v) mismatch (-want +got):\n%s", runnerName, diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
var runnerName string = "self-hosted-ubuntu"
|
||||||
|
retryClient := retryablehttp.NewClient()
|
||||||
|
retryClient.RetryWaitMax = 1 * time.Millisecond
|
||||||
|
retryClient.RetryMax = 1
|
||||||
|
|
||||||
|
actualRetry := 0
|
||||||
|
expectedRetry := retryClient.RetryMax + 1
|
||||||
|
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
httpClient := retryClient.StandardClient()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
Client: httpClient,
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = actionsClient.GetRunnerByName(context.Background(), runnerName)
|
||||||
|
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteRunner(t *testing.T) {
|
||||||
|
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
|
||||||
|
|
||||||
|
t.Run("Delete Runner", func(t *testing.T) {
|
||||||
|
var runnerID int64 = 1
|
||||||
|
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := actionsClient.RemoveRunner(context.Background(), runnerID); err != nil {
|
||||||
|
t.Fatalf("RemoveRunner got unexepected error, %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
var runnerID int64 = 1
|
||||||
|
|
||||||
|
retryClient := retryablehttp.NewClient()
|
||||||
|
retryClient.RetryWaitMax = 1 * time.Millisecond
|
||||||
|
retryClient.RetryMax = 1
|
||||||
|
|
||||||
|
actualRetry := 0
|
||||||
|
expectedRetry := retryClient.RetryMax + 1
|
||||||
|
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
httpClient := retryClient.StandardClient()
|
||||||
|
actionsClient := actions.Client{
|
||||||
|
Client: httpClient,
|
||||||
|
ActionsServiceURL: &s.URL,
|
||||||
|
ActionsServiceAdminToken: &token,
|
||||||
|
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = actionsClient.RemoveRunner(context.Background(), runnerID)
|
||||||
|
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
})
|
||||||
|
}
|
||||||
71
github/actions/errors.go
Normal file
71
github/actions/errors.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActionsError struct {
|
||||||
|
ExceptionName string `json:"typeName,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ActionsError) Error() string {
|
||||||
|
return fmt.Sprintf("%v - had issue communicating with Actions backend: %v", e.StatusCode, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseActionsErrorFromResponse(response *http.Response) error {
|
||||||
|
if response.ContentLength == 0 {
|
||||||
|
message := "Request returned status: " + response.Status
|
||||||
|
return &ActionsError{
|
||||||
|
ExceptionName: "unknown",
|
||||||
|
Message: message,
|
||||||
|
StatusCode: response.StatusCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defer response.Body.Close()
|
||||||
|
body, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
body = trimByteOrderMark(body)
|
||||||
|
contentType, ok := response.Header["Content-Type"]
|
||||||
|
if ok && len(contentType) > 0 && strings.Contains(contentType[0], "text/plain") {
|
||||||
|
message := string(body)
|
||||||
|
statusCode := response.StatusCode
|
||||||
|
return &ActionsError{
|
||||||
|
Message: message,
|
||||||
|
StatusCode: statusCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actionsError := &ActionsError{StatusCode: response.StatusCode}
|
||||||
|
if err := json.Unmarshal(body, &actionsError); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return actionsError
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageQueueTokenExpiredError struct {
|
||||||
|
msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *MessageQueueTokenExpiredError) Error() string {
|
||||||
|
return e.msg
|
||||||
|
}
|
||||||
|
|
||||||
|
type HttpClientSideError struct {
|
||||||
|
msg string
|
||||||
|
Code int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HttpClientSideError) Error() string {
|
||||||
|
return e.msg
|
||||||
|
}
|
||||||
235
github/actions/fake/client.go
Normal file
235
github/actions/fake/client.go
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
package fake
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Option func(*FakeClient)
|
||||||
|
|
||||||
|
func WithGetRunnerScaleSetResult(scaleSet *actions.RunnerScaleSet, err error) Option {
|
||||||
|
return func(f *FakeClient) {
|
||||||
|
f.getRunnerScaleSetResult.RunnerScaleSet = scaleSet
|
||||||
|
f.getRunnerScaleSetResult.err = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithGetRunner(runner *actions.RunnerReference, err error) Option {
|
||||||
|
return func(f *FakeClient) {
|
||||||
|
f.getRunnerResult.RunnerReference = runner
|
||||||
|
f.getRunnerResult.err = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultRunnerScaleSet = &actions.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
Name: "testset",
|
||||||
|
RunnerGroupId: 1,
|
||||||
|
RunnerGroupName: "testgroup",
|
||||||
|
Labels: []actions.Label{{Type: "test", Name: "test"}},
|
||||||
|
RunnerSetting: actions.RunnerSetting{},
|
||||||
|
CreatedOn: time.Now(),
|
||||||
|
RunnerJitConfigUrl: "test.test.test",
|
||||||
|
Statistics: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultRunnerGroup = &actions.RunnerGroup{
|
||||||
|
ID: 1,
|
||||||
|
Name: "testgroup",
|
||||||
|
Size: 1,
|
||||||
|
IsDefault: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var sessionID = uuid.New()
|
||||||
|
|
||||||
|
var defaultRunnerScaleSetSession = &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &sessionID,
|
||||||
|
OwnerName: "testowner",
|
||||||
|
RunnerScaleSet: defaultRunnerScaleSet,
|
||||||
|
MessageQueueUrl: "https://test.url/path",
|
||||||
|
MessageQueueAccessToken: "faketoken",
|
||||||
|
Statistics: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultAcquirableJob = &actions.AcquirableJob{
|
||||||
|
AcquireJobUrl: "https://test.url",
|
||||||
|
MessageType: "",
|
||||||
|
RunnerRequestId: 1,
|
||||||
|
RepositoryName: "testrepo",
|
||||||
|
OwnerName: "testowner",
|
||||||
|
JobWorkflowRef: "workflowref",
|
||||||
|
EventName: "testevent",
|
||||||
|
RequestLabels: []string{"test"},
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultAcquirableJobList = &actions.AcquirableJobList{
|
||||||
|
Count: 1,
|
||||||
|
Jobs: []actions.AcquirableJob{*defaultAcquirableJob},
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultRunnerReference = &actions.RunnerReference{
|
||||||
|
Id: 1,
|
||||||
|
Name: "testrunner",
|
||||||
|
RunnerScaleSetId: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultRunnerScaleSetMessage = &actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "test",
|
||||||
|
Body: "{}",
|
||||||
|
Statistics: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultRunnerScaleSetJitRunnerConfig = &actions.RunnerScaleSetJitRunnerConfig{
|
||||||
|
Runner: defaultRunnerReference,
|
||||||
|
EncodedJITConfig: "test",
|
||||||
|
}
|
||||||
|
|
||||||
|
// FakeClient implements actions service
|
||||||
|
type FakeClient struct {
|
||||||
|
getRunnerScaleSetResult struct {
|
||||||
|
*actions.RunnerScaleSet
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
getRunnerScaleSetByIdResult struct {
|
||||||
|
*actions.RunnerScaleSet
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
getRunnerGroupByNameResult struct {
|
||||||
|
*actions.RunnerGroup
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
createRunnerScaleSetResult struct {
|
||||||
|
*actions.RunnerScaleSet
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
createMessageSessionResult struct {
|
||||||
|
*actions.RunnerScaleSetSession
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
deleteMessageSessionResult struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
refreshMessageSessionResult struct {
|
||||||
|
*actions.RunnerScaleSetSession
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
acquireJobsResult struct {
|
||||||
|
ids []int64
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
getAcquirableJobsResult struct {
|
||||||
|
*actions.AcquirableJobList
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
getMessageResult struct {
|
||||||
|
*actions.RunnerScaleSetMessage
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
deleteMessageResult struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
generateJitRunnerConfigResult struct {
|
||||||
|
*actions.RunnerScaleSetJitRunnerConfig
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
getRunnerResult struct {
|
||||||
|
*actions.RunnerReference
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
getRunnerByNameResult struct {
|
||||||
|
*actions.RunnerReference
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
removeRunnerResult struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFakeClient(options ...Option) actions.ActionsService {
|
||||||
|
f := &FakeClient{}
|
||||||
|
f.applyDefaults()
|
||||||
|
for _, opt := range options {
|
||||||
|
opt(f)
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeClient) applyDefaults() {
|
||||||
|
f.getRunnerScaleSetResult.RunnerScaleSet = defaultRunnerScaleSet
|
||||||
|
f.getRunnerScaleSetByIdResult.RunnerScaleSet = defaultRunnerScaleSet
|
||||||
|
f.getRunnerGroupByNameResult.RunnerGroup = defaultRunnerGroup
|
||||||
|
f.createRunnerScaleSetResult.RunnerScaleSet = defaultRunnerScaleSet
|
||||||
|
f.createMessageSessionResult.RunnerScaleSetSession = defaultRunnerScaleSetSession
|
||||||
|
f.refreshMessageSessionResult.RunnerScaleSetSession = defaultRunnerScaleSetSession
|
||||||
|
f.acquireJobsResult.ids = []int64{1}
|
||||||
|
f.getAcquirableJobsResult.AcquirableJobList = defaultAcquirableJobList
|
||||||
|
f.getMessageResult.RunnerScaleSetMessage = defaultRunnerScaleSetMessage
|
||||||
|
f.generateJitRunnerConfigResult.RunnerScaleSetJitRunnerConfig = defaultRunnerScaleSetJitRunnerConfig
|
||||||
|
f.getRunnerResult.RunnerReference = defaultRunnerReference
|
||||||
|
f.getRunnerByNameResult.RunnerReference = defaultRunnerReference
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeClient) GetRunnerScaleSet(ctx context.Context, runnerScaleSetName string) (*actions.RunnerScaleSet, error) {
|
||||||
|
return f.getRunnerScaleSetResult.RunnerScaleSet, f.getRunnerScaleSetResult.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeClient) GetRunnerScaleSetById(ctx context.Context, runnerScaleSetId int) (*actions.RunnerScaleSet, error) {
|
||||||
|
return f.getRunnerScaleSetByIdResult.RunnerScaleSet, f.getRunnerScaleSetResult.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeClient) GetRunnerGroupByName(ctx context.Context, runnerGroup string) (*actions.RunnerGroup, error) {
|
||||||
|
return f.getRunnerGroupByNameResult.RunnerGroup, f.getRunnerGroupByNameResult.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeClient) CreateRunnerScaleSet(ctx context.Context, runnerScaleSet *actions.RunnerScaleSet) (*actions.RunnerScaleSet, error) {
|
||||||
|
return f.createRunnerScaleSetResult.RunnerScaleSet, f.createRunnerScaleSetResult.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeClient) CreateMessageSession(ctx context.Context, runnerScaleSetId int, owner string) (*actions.RunnerScaleSetSession, error) {
|
||||||
|
return f.createMessageSessionResult.RunnerScaleSetSession, f.createMessageSessionResult.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeClient) DeleteMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) error {
|
||||||
|
return f.deleteMessageSessionResult.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeClient) RefreshMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) (*actions.RunnerScaleSetSession, error) {
|
||||||
|
return f.refreshMessageSessionResult.RunnerScaleSetSession, f.refreshMessageSessionResult.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeClient) AcquireJobs(ctx context.Context, runnerScaleSetId int, messageQueueAccessToken string, requestIds []int64) ([]int64, error) {
|
||||||
|
return f.acquireJobsResult.ids, f.acquireJobsResult.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeClient) GetAcquirableJobs(ctx context.Context, runnerScaleSetId int) (*actions.AcquirableJobList, error) {
|
||||||
|
return f.getAcquirableJobsResult.AcquirableJobList, f.getAcquirableJobsResult.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeClient) GetMessage(ctx context.Context, messageQueueUrl, messageQueueAccessToken string, lastMessageId int64) (*actions.RunnerScaleSetMessage, error) {
|
||||||
|
return f.getMessageResult.RunnerScaleSetMessage, f.getMessageResult.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeClient) DeleteMessage(ctx context.Context, messageQueueUrl, messageQueueAccessToken string, messageId int64) error {
|
||||||
|
return f.deleteMessageResult.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeClient) GenerateJitRunnerConfig(ctx context.Context, jitRunnerSetting *actions.RunnerScaleSetJitRunnerSetting, scaleSetId int) (*actions.RunnerScaleSetJitRunnerConfig, error) {
|
||||||
|
return f.generateJitRunnerConfigResult.RunnerScaleSetJitRunnerConfig, f.generateJitRunnerConfigResult.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeClient) GetRunner(ctx context.Context, runnerId int64) (*actions.RunnerReference, error) {
|
||||||
|
return f.getRunnerResult.RunnerReference, f.getRunnerResult.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeClient) GetRunnerByName(ctx context.Context, runnerName string) (*actions.RunnerReference, error) {
|
||||||
|
return f.getRunnerByNameResult.RunnerReference, f.getRunnerByNameResult.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeClient) RemoveRunner(ctx context.Context, runnerId int64) error {
|
||||||
|
return f.removeRunnerResult.err
|
||||||
|
}
|
||||||
43
github/actions/fake/multi_client.go
Normal file
43
github/actions/fake/multi_client.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package fake
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MultiClientOption func(*fakeMultiClient)
|
||||||
|
|
||||||
|
func WithDefaultClient(client actions.ActionsService, err error) MultiClientOption {
|
||||||
|
return func(f *fakeMultiClient) {
|
||||||
|
f.defaultClient = client
|
||||||
|
f.defaultErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeMultiClient struct {
|
||||||
|
defaultClient actions.ActionsService
|
||||||
|
defaultErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMultiClient(opts ...MultiClientOption) actions.MultiClient {
|
||||||
|
f := &fakeMultiClient{}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.defaultClient == nil {
|
||||||
|
f.defaultClient = NewFakeClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeMultiClient) GetClientFor(ctx context.Context, githubConfigURL string, creds actions.ActionsAuth, namespace string) (actions.ActionsService, error) {
|
||||||
|
return f.defaultClient, f.defaultErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeMultiClient) GetClientFromSecret(ctx context.Context, githubConfigURL, namespace string, secretData actions.KubernetesSecretData) (actions.ActionsService, error) {
|
||||||
|
return f.defaultClient, f.defaultErr
|
||||||
|
}
|
||||||
348
github/actions/mock_ActionsService.go
Normal file
348
github/actions/mock_ActionsService.go
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
// Code generated by mockery v2.16.0. DO NOT EDIT.
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
|
||||||
|
uuid "github.com/google/uuid"
|
||||||
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockActionsService is an autogenerated mock type for the ActionsService type
|
||||||
|
type MockActionsService struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcquireJobs provides a mock function with given fields: ctx, runnerScaleSetId, messageQueueAccessToken, requestIds
|
||||||
|
func (_m *MockActionsService) AcquireJobs(ctx context.Context, runnerScaleSetId int, messageQueueAccessToken string, requestIds []int64) ([]int64, error) {
|
||||||
|
ret := _m.Called(ctx, runnerScaleSetId, messageQueueAccessToken, requestIds)
|
||||||
|
|
||||||
|
var r0 []int64
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int, string, []int64) []int64); ok {
|
||||||
|
r0 = rf(ctx, runnerScaleSetId, messageQueueAccessToken, requestIds)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]int64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, int, string, []int64) error); ok {
|
||||||
|
r1 = rf(ctx, runnerScaleSetId, messageQueueAccessToken, requestIds)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMessageSession provides a mock function with given fields: ctx, runnerScaleSetId, owner
|
||||||
|
func (_m *MockActionsService) CreateMessageSession(ctx context.Context, runnerScaleSetId int, owner string) (*RunnerScaleSetSession, error) {
|
||||||
|
ret := _m.Called(ctx, runnerScaleSetId, owner)
|
||||||
|
|
||||||
|
var r0 *RunnerScaleSetSession
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int, string) *RunnerScaleSetSession); ok {
|
||||||
|
r0 = rf(ctx, runnerScaleSetId, owner)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*RunnerScaleSetSession)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, int, string) error); ok {
|
||||||
|
r1 = rf(ctx, runnerScaleSetId, owner)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRunnerScaleSet provides a mock function with given fields: ctx, runnerScaleSet
|
||||||
|
func (_m *MockActionsService) CreateRunnerScaleSet(ctx context.Context, runnerScaleSet *RunnerScaleSet) (*RunnerScaleSet, error) {
|
||||||
|
ret := _m.Called(ctx, runnerScaleSet)
|
||||||
|
|
||||||
|
var r0 *RunnerScaleSet
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *RunnerScaleSet) *RunnerScaleSet); ok {
|
||||||
|
r0 = rf(ctx, runnerScaleSet)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*RunnerScaleSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, *RunnerScaleSet) error); ok {
|
||||||
|
r1 = rf(ctx, runnerScaleSet)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMessage provides a mock function with given fields: ctx, messageQueueUrl, messageQueueAccessToken, messageId
|
||||||
|
func (_m *MockActionsService) DeleteMessage(ctx context.Context, messageQueueUrl string, messageQueueAccessToken string, messageId int64) error {
|
||||||
|
ret := _m.Called(ctx, messageQueueUrl, messageQueueAccessToken, messageId)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) error); ok {
|
||||||
|
r0 = rf(ctx, messageQueueUrl, messageQueueAccessToken, messageId)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMessageSession provides a mock function with given fields: ctx, runnerScaleSetId, sessionId
|
||||||
|
func (_m *MockActionsService) DeleteMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) error {
|
||||||
|
ret := _m.Called(ctx, runnerScaleSetId, sessionId)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int, *uuid.UUID) error); ok {
|
||||||
|
r0 = rf(ctx, runnerScaleSetId, sessionId)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateJitRunnerConfig provides a mock function with given fields: ctx, jitRunnerSetting, scaleSetId
|
||||||
|
func (_m *MockActionsService) GenerateJitRunnerConfig(ctx context.Context, jitRunnerSetting *RunnerScaleSetJitRunnerSetting, scaleSetId int) (*RunnerScaleSetJitRunnerConfig, error) {
|
||||||
|
ret := _m.Called(ctx, jitRunnerSetting, scaleSetId)
|
||||||
|
|
||||||
|
var r0 *RunnerScaleSetJitRunnerConfig
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *RunnerScaleSetJitRunnerSetting, int) *RunnerScaleSetJitRunnerConfig); ok {
|
||||||
|
r0 = rf(ctx, jitRunnerSetting, scaleSetId)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*RunnerScaleSetJitRunnerConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, *RunnerScaleSetJitRunnerSetting, int) error); ok {
|
||||||
|
r1 = rf(ctx, jitRunnerSetting, scaleSetId)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAcquirableJobs provides a mock function with given fields: ctx, runnerScaleSetId
|
||||||
|
func (_m *MockActionsService) GetAcquirableJobs(ctx context.Context, runnerScaleSetId int) (*AcquirableJobList, error) {
|
||||||
|
ret := _m.Called(ctx, runnerScaleSetId)
|
||||||
|
|
||||||
|
var r0 *AcquirableJobList
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int) *AcquirableJobList); ok {
|
||||||
|
r0 = rf(ctx, runnerScaleSetId)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*AcquirableJobList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
|
||||||
|
r1 = rf(ctx, runnerScaleSetId)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMessage provides a mock function with given fields: ctx, messageQueueUrl, messageQueueAccessToken, lastMessageId
|
||||||
|
func (_m *MockActionsService) GetMessage(ctx context.Context, messageQueueUrl string, messageQueueAccessToken string, lastMessageId int64) (*RunnerScaleSetMessage, error) {
|
||||||
|
ret := _m.Called(ctx, messageQueueUrl, messageQueueAccessToken, lastMessageId)
|
||||||
|
|
||||||
|
var r0 *RunnerScaleSetMessage
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) *RunnerScaleSetMessage); ok {
|
||||||
|
r0 = rf(ctx, messageQueueUrl, messageQueueAccessToken, lastMessageId)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*RunnerScaleSetMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, string, string, int64) error); ok {
|
||||||
|
r1 = rf(ctx, messageQueueUrl, messageQueueAccessToken, lastMessageId)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRunner provides a mock function with given fields: ctx, runnerId
|
||||||
|
func (_m *MockActionsService) GetRunner(ctx context.Context, runnerId int64) (*RunnerReference, error) {
|
||||||
|
ret := _m.Called(ctx, runnerId)
|
||||||
|
|
||||||
|
var r0 *RunnerReference
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int64) *RunnerReference); ok {
|
||||||
|
r0 = rf(ctx, runnerId)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*RunnerReference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||||
|
r1 = rf(ctx, runnerId)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRunnerByName provides a mock function with given fields: ctx, runnerName
|
||||||
|
func (_m *MockActionsService) GetRunnerByName(ctx context.Context, runnerName string) (*RunnerReference, error) {
|
||||||
|
ret := _m.Called(ctx, runnerName)
|
||||||
|
|
||||||
|
var r0 *RunnerReference
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string) *RunnerReference); ok {
|
||||||
|
r0 = rf(ctx, runnerName)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*RunnerReference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||||
|
r1 = rf(ctx, runnerName)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRunnerGroupByName provides a mock function with given fields: ctx, runnerGroup
|
||||||
|
func (_m *MockActionsService) GetRunnerGroupByName(ctx context.Context, runnerGroup string) (*RunnerGroup, error) {
|
||||||
|
ret := _m.Called(ctx, runnerGroup)
|
||||||
|
|
||||||
|
var r0 *RunnerGroup
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string) *RunnerGroup); ok {
|
||||||
|
r0 = rf(ctx, runnerGroup)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*RunnerGroup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||||
|
r1 = rf(ctx, runnerGroup)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRunnerScaleSet provides a mock function with given fields: ctx, runnerScaleSetName
|
||||||
|
func (_m *MockActionsService) GetRunnerScaleSet(ctx context.Context, runnerScaleSetName string) (*RunnerScaleSet, error) {
|
||||||
|
ret := _m.Called(ctx, runnerScaleSetName)
|
||||||
|
|
||||||
|
var r0 *RunnerScaleSet
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string) *RunnerScaleSet); ok {
|
||||||
|
r0 = rf(ctx, runnerScaleSetName)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*RunnerScaleSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||||
|
r1 = rf(ctx, runnerScaleSetName)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRunnerScaleSetById provides a mock function with given fields: ctx, runnerScaleSetId
|
||||||
|
func (_m *MockActionsService) GetRunnerScaleSetById(ctx context.Context, runnerScaleSetId int) (*RunnerScaleSet, error) {
|
||||||
|
ret := _m.Called(ctx, runnerScaleSetId)
|
||||||
|
|
||||||
|
var r0 *RunnerScaleSet
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int) *RunnerScaleSet); ok {
|
||||||
|
r0 = rf(ctx, runnerScaleSetId)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*RunnerScaleSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
|
||||||
|
r1 = rf(ctx, runnerScaleSetId)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshMessageSession provides a mock function with given fields: ctx, runnerScaleSetId, sessionId
|
||||||
|
func (_m *MockActionsService) RefreshMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) (*RunnerScaleSetSession, error) {
|
||||||
|
ret := _m.Called(ctx, runnerScaleSetId, sessionId)
|
||||||
|
|
||||||
|
var r0 *RunnerScaleSetSession
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int, *uuid.UUID) *RunnerScaleSetSession); ok {
|
||||||
|
r0 = rf(ctx, runnerScaleSetId, sessionId)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*RunnerScaleSetSession)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, int, *uuid.UUID) error); ok {
|
||||||
|
r1 = rf(ctx, runnerScaleSetId, sessionId)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRunner provides a mock function with given fields: ctx, runnerId
|
||||||
|
func (_m *MockActionsService) RemoveRunner(ctx context.Context, runnerId int64) error {
|
||||||
|
ret := _m.Called(ctx, runnerId)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
|
||||||
|
r0 = rf(ctx, runnerId)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockConstructorTestingTNewMockActionsService interface {
|
||||||
|
mock.TestingT
|
||||||
|
Cleanup(func())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockActionsService creates a new instance of MockActionsService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||||
|
func NewMockActionsService(t mockConstructorTestingTNewMockActionsService) *MockActionsService {
|
||||||
|
mock := &MockActionsService{}
|
||||||
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
103
github/actions/mock_SessionService.go
Normal file
103
github/actions/mock_SessionService.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
// Code generated by mockery v2.16.0. DO NOT EDIT.
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
|
||||||
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockSessionService is an autogenerated mock type for the SessionService type
|
||||||
|
type MockSessionService struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcquireJobs provides a mock function with given fields: ctx, requestIds
|
||||||
|
func (_m *MockSessionService) AcquireJobs(ctx context.Context, requestIds []int64) ([]int64, error) {
|
||||||
|
ret := _m.Called(ctx, requestIds)
|
||||||
|
|
||||||
|
var r0 []int64
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, []int64) []int64); ok {
|
||||||
|
r0 = rf(ctx, requestIds)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]int64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, []int64) error); ok {
|
||||||
|
r1 = rf(ctx, requestIds)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close provides a mock function with given fields:
|
||||||
|
func (_m *MockSessionService) Close() error {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func() error); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMessage provides a mock function with given fields: ctx, messageId
|
||||||
|
func (_m *MockSessionService) DeleteMessage(ctx context.Context, messageId int64) error {
|
||||||
|
ret := _m.Called(ctx, messageId)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
|
||||||
|
r0 = rf(ctx, messageId)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMessage provides a mock function with given fields: ctx, lastMessageId
|
||||||
|
func (_m *MockSessionService) GetMessage(ctx context.Context, lastMessageId int64) (*RunnerScaleSetMessage, error) {
|
||||||
|
ret := _m.Called(ctx, lastMessageId)
|
||||||
|
|
||||||
|
var r0 *RunnerScaleSetMessage
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int64) *RunnerScaleSetMessage); ok {
|
||||||
|
r0 = rf(ctx, lastMessageId)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*RunnerScaleSetMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
|
||||||
|
r1 = rf(ctx, lastMessageId)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockConstructorTestingTNewMockSessionService interface {
|
||||||
|
mock.TestingT
|
||||||
|
Cleanup(func())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockSessionService creates a new instance of MockSessionService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||||
|
func NewMockSessionService(t mockConstructorTestingTNewMockSessionService) *MockSessionService {
|
||||||
|
mock := &MockSessionService{}
|
||||||
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
164
github/actions/multi_client.go
Normal file
164
github/actions/multi_client.go
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MultiClient interface {
|
||||||
|
GetClientFor(ctx context.Context, githubConfigURL string, creds ActionsAuth, namespace string) (ActionsService, error)
|
||||||
|
GetClientFromSecret(ctx context.Context, githubConfigURL, namespace string, secretData KubernetesSecretData) (ActionsService, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type multiClient struct {
|
||||||
|
// To lock adding and removing of individual clients.
|
||||||
|
mu sync.Mutex
|
||||||
|
clients map[ActionsClientKey]*actionsClientWrapper
|
||||||
|
|
||||||
|
logger logr.Logger
|
||||||
|
userAgent string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GitHubAppAuth struct {
|
||||||
|
AppID int64
|
||||||
|
AppInstallationID int64
|
||||||
|
AppPrivateKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionsAuth struct {
|
||||||
|
// GitHub App
|
||||||
|
AppCreds *GitHubAppAuth
|
||||||
|
|
||||||
|
// GitHub PAT
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionsClientKey struct {
|
||||||
|
ActionsURL string
|
||||||
|
Auth ActionsAuth
|
||||||
|
Namespace string
|
||||||
|
}
|
||||||
|
|
||||||
|
type actionsClientWrapper struct {
|
||||||
|
// To lock client usage when tokens are being refreshed.
|
||||||
|
mu sync.Mutex
|
||||||
|
|
||||||
|
client ActionsService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMultiClient(userAgent string, logger logr.Logger) MultiClient {
|
||||||
|
return &multiClient{
|
||||||
|
mu: sync.Mutex{},
|
||||||
|
clients: make(map[ActionsClientKey]*actionsClientWrapper),
|
||||||
|
logger: logger,
|
||||||
|
userAgent: userAgent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *multiClient) GetClientFor(ctx context.Context, githubConfigURL string, creds ActionsAuth, namespace string) (ActionsService, error) {
|
||||||
|
m.logger.Info("retrieve actions client", "githubConfigURL", githubConfigURL, "namespace", namespace)
|
||||||
|
|
||||||
|
parsedGitHubURL, err := url.Parse(githubConfigURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if creds.Token == "" && creds.AppCreds == nil {
|
||||||
|
return nil, fmt.Errorf("no credentials provided. either a PAT or GitHub App credentials should be provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
if creds.Token != "" && creds.AppCreds != nil {
|
||||||
|
return nil, fmt.Errorf("both PAT and GitHub App credentials provided. should only provide one")
|
||||||
|
}
|
||||||
|
|
||||||
|
key := ActionsClientKey{
|
||||||
|
ActionsURL: parsedGitHubURL.String(),
|
||||||
|
Namespace: namespace,
|
||||||
|
}
|
||||||
|
|
||||||
|
if creds.AppCreds != nil {
|
||||||
|
key.Auth = ActionsAuth{
|
||||||
|
AppCreds: creds.AppCreds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if creds.Token != "" {
|
||||||
|
key.Auth = ActionsAuth{
|
||||||
|
Token: creds.Token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
clientWrapper, has := m.clients[key]
|
||||||
|
if has {
|
||||||
|
m.logger.Info("using cache client", "githubConfigURL", githubConfigURL, "namespace", namespace)
|
||||||
|
return clientWrapper.client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.logger.Info("creating new client", "githubConfigURL", githubConfigURL, "namespace", namespace)
|
||||||
|
|
||||||
|
client, err := NewClient(ctx, githubConfigURL, &creds, m.userAgent, m.logger)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.clients[key] = &actionsClientWrapper{
|
||||||
|
mu: sync.Mutex{},
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.logger.Info("successfully created new client", "githubConfigURL", githubConfigURL, "namespace", namespace)
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type KubernetesSecretData map[string][]byte
|
||||||
|
|
||||||
|
func (m *multiClient) GetClientFromSecret(ctx context.Context, githubConfigURL, namespace string, secretData KubernetesSecretData) (ActionsService, error) {
|
||||||
|
if len(secretData) == 0 {
|
||||||
|
return nil, fmt.Errorf("must provide secret data with either PAT or GitHub App Auth")
|
||||||
|
}
|
||||||
|
|
||||||
|
token := string(secretData["github_token"])
|
||||||
|
hasToken := len(token) > 0
|
||||||
|
|
||||||
|
appID := string(secretData["github_app_id"])
|
||||||
|
appInstallationID := string(secretData["github_app_installation_id"])
|
||||||
|
appPrivateKey := string(secretData["github_app_private_key"])
|
||||||
|
hasGitHubAppAuth := len(appID) > 0 && len(appInstallationID) > 0 && len(appPrivateKey) > 0
|
||||||
|
|
||||||
|
if hasToken && hasGitHubAppAuth {
|
||||||
|
return nil, fmt.Errorf("must provide secret with only PAT or GitHub App Auth to avoid ambiguity in client behavior")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasToken && !hasGitHubAppAuth {
|
||||||
|
return nil, fmt.Errorf("neither PAT nor GitHub App Auth credentials provided in secret")
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := ActionsAuth{}
|
||||||
|
|
||||||
|
if hasToken {
|
||||||
|
auth.Token = token
|
||||||
|
return m.GetClientFor(ctx, githubConfigURL, auth, namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedAppID, err := strconv.ParseInt(appID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedAppInstallationID, err := strconv.ParseInt(appInstallationID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.AppCreds = &GitHubAppAuth{AppID: parsedAppID, AppInstallationID: parsedAppInstallationID, AppPrivateKey: appPrivateKey}
|
||||||
|
return m.GetClientFor(ctx, githubConfigURL, auth, namespace)
|
||||||
|
}
|
||||||
163
github/actions/multi_client_test.go
Normal file
163
github/actions/multi_client_test.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAddClient(t *testing.T) {
|
||||||
|
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: creating logger: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
multiClient := NewMultiClient("test-user-agent", logger).(*multiClient)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.HasSuffix(r.URL.Path, "actions/runners/registration-token") {
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
token := "abc-123"
|
||||||
|
rt := ®istrationToken{Token: &token}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(rt); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(r.URL.Path, "actions/runner-registration") {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
url := "actions.github.com/abc"
|
||||||
|
jwt := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
|
||||||
|
adminConnInfo := &ActionsServiceAdminConnection{ActionsServiceUrl: &url, AdminToken: &jwt}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(adminConnInfo); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(r.URL.Path, "/access_tokens") {
|
||||||
|
w.Header().Set("Content-Type", "application/vnd.github+json")
|
||||||
|
|
||||||
|
t, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z07:00")
|
||||||
|
accessToken := &accessToken{
|
||||||
|
Token: "abc-123",
|
||||||
|
ExpiresAt: t,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(accessToken); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
want := 1
|
||||||
|
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github/github", srv.URL), ActionsAuth{Token: "PAT"}, "namespace"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want++ // New repo
|
||||||
|
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github/actions", srv.URL), ActionsAuth{Token: "PAT"}, "namespace"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repeat
|
||||||
|
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github/github", srv.URL), ActionsAuth{Token: "PAT"}, "namespace"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want++ // New namespace
|
||||||
|
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github/github", srv.URL), ActionsAuth{Token: "PAT"}, "other"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want++ // New pat
|
||||||
|
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github/github", srv.URL), ActionsAuth{Token: "other"}, "other"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want++ // New org
|
||||||
|
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github", srv.URL), ActionsAuth{Token: "PAT"}, "other"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// No org, repo, enterprise
|
||||||
|
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v", srv.URL), ActionsAuth{Token: "PAT"}, "other"); err == nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want++ // Test keying on GitHub App
|
||||||
|
appAuth := &GitHubAppAuth{
|
||||||
|
AppID: 1,
|
||||||
|
AppPrivateKey: `-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIICWgIBAAKBgHXfRT9cv9UY9fAAD4+1RshpfSSZe277urfEmPfX3/Og9zJYRk//
|
||||||
|
CZrJVD1CaBZDiIyQsNEzjta7r4UsqWdFOggiNN2E7ZTFQjMSaFkVgrzHqWuiaCBf
|
||||||
|
/BjbKPn4SMDmTzHvIe7Nel76hBdCaVgu6mYCW5jmuSH5qz/yR1U1J/WJAgMBAAEC
|
||||||
|
gYARWGWsSU3BYgbu5lNj5l0gKMXNmPhdAJYdbMTF0/KUu18k/XB7XSBgsre+vALt
|
||||||
|
I8r4RGKApoGif8P4aPYUyE8dqA1bh0X3Fj1TCz28qoUL5//dA+pigCRS20H7HM3C
|
||||||
|
ojoqF7+F+4F2sXmzFNd1NgY5RxFPYosTT7OnUiFuu2IisQJBALnMLe09LBnjuHXR
|
||||||
|
xxR65DDNxWPQLBjW3dL+ubLcwr7922l6ZIQsVjdeE0ItEUVRjjJ9/B/Jq9VJ/Lw4
|
||||||
|
g9LCkkMCQQCiaM2f7nYmGivPo9hlAbq5lcGJ5CCYFfeeYzTxMqum7Mbqe4kk5lgb
|
||||||
|
X6gWd0Izg2nGdAEe/97DClO6VpKcPbpDAkBTR/JOJN1fvXMxXJaf13XxakrQMr+R
|
||||||
|
Yr6LlSInykyAz8lJvlLP7A+5QbHgN9NF/wh+GXqpxPwA3ukqdSqhjhWBAkBn6mDv
|
||||||
|
HPgR5xrzL6XM8y9TgaOlJAdK6HtYp6d/UOmN0+Butf6JUq07TphRT5tXNJVgemch
|
||||||
|
O5x/9UKfbrc+KyzbAkAo97TfFC+mZhU1N5fFelaRu4ikPxlp642KRUSkOh8GEkNf
|
||||||
|
jQ97eJWiWtDcsMUhcZgoB5ydHcFlrBIn6oBcpge5
|
||||||
|
-----END RSA PRIVATE KEY-----`,
|
||||||
|
}
|
||||||
|
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github/github", srv.URL), ActionsAuth{AppCreds: appAuth}, "other"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repeat last to verify GitHub App keys are mapped together
|
||||||
|
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github/github", srv.URL), ActionsAuth{AppCreds: appAuth}, "other"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(multiClient.clients) != want {
|
||||||
|
t.Fatalf("GetClientFor: unexpected number of clients: got=%v want=%v", len(multiClient.clients), want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateJWT(t *testing.T) {
|
||||||
|
key := `-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIICWgIBAAKBgHXfRT9cv9UY9fAAD4+1RshpfSSZe277urfEmPfX3/Og9zJYRk//
|
||||||
|
CZrJVD1CaBZDiIyQsNEzjta7r4UsqWdFOggiNN2E7ZTFQjMSaFkVgrzHqWuiaCBf
|
||||||
|
/BjbKPn4SMDmTzHvIe7Nel76hBdCaVgu6mYCW5jmuSH5qz/yR1U1J/WJAgMBAAEC
|
||||||
|
gYARWGWsSU3BYgbu5lNj5l0gKMXNmPhdAJYdbMTF0/KUu18k/XB7XSBgsre+vALt
|
||||||
|
I8r4RGKApoGif8P4aPYUyE8dqA1bh0X3Fj1TCz28qoUL5//dA+pigCRS20H7HM3C
|
||||||
|
ojoqF7+F+4F2sXmzFNd1NgY5RxFPYosTT7OnUiFuu2IisQJBALnMLe09LBnjuHXR
|
||||||
|
xxR65DDNxWPQLBjW3dL+ubLcwr7922l6ZIQsVjdeE0ItEUVRjjJ9/B/Jq9VJ/Lw4
|
||||||
|
g9LCkkMCQQCiaM2f7nYmGivPo9hlAbq5lcGJ5CCYFfeeYzTxMqum7Mbqe4kk5lgb
|
||||||
|
X6gWd0Izg2nGdAEe/97DClO6VpKcPbpDAkBTR/JOJN1fvXMxXJaf13XxakrQMr+R
|
||||||
|
Yr6LlSInykyAz8lJvlLP7A+5QbHgN9NF/wh+GXqpxPwA3ukqdSqhjhWBAkBn6mDv
|
||||||
|
HPgR5xrzL6XM8y9TgaOlJAdK6HtYp6d/UOmN0+Butf6JUq07TphRT5tXNJVgemch
|
||||||
|
O5x/9UKfbrc+KyzbAkAo97TfFC+mZhU1N5fFelaRu4ikPxlp642KRUSkOh8GEkNf
|
||||||
|
jQ97eJWiWtDcsMUhcZgoB5ydHcFlrBIn6oBcpge5
|
||||||
|
-----END RSA PRIVATE KEY-----`
|
||||||
|
|
||||||
|
auth := &GitHubAppAuth{
|
||||||
|
AppID: 123,
|
||||||
|
AppPrivateKey: key,
|
||||||
|
}
|
||||||
|
jwt, err := createJWTForGitHubApp(auth)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Println(jwt)
|
||||||
|
}
|
||||||
14
github/actions/sessionservice.go
Normal file
14
github/actions/sessionservice.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate mockery --inpackage --name=SessionService
|
||||||
|
type SessionService interface {
|
||||||
|
GetMessage(ctx context.Context, lastMessageId int64) (*RunnerScaleSetMessage, error)
|
||||||
|
DeleteMessage(ctx context.Context, messageId int64) error
|
||||||
|
AcquireJobs(ctx context.Context, requestIds []int64) ([]int64, error)
|
||||||
|
io.Closer
|
||||||
|
}
|
||||||
153
github/actions/types.go
Normal file
153
github/actions/types.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AcquirableJobList struct {
|
||||||
|
Count int `json:"count"`
|
||||||
|
Jobs []AcquirableJob `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AcquirableJob struct {
|
||||||
|
AcquireJobUrl string `json:"acquireJobUrl"`
|
||||||
|
MessageType string `json:"messageType"`
|
||||||
|
RunnerRequestId int64 `json:"runnerRequestId"`
|
||||||
|
RepositoryName string `json:"repositoryName"`
|
||||||
|
OwnerName string `json:"ownerName"`
|
||||||
|
JobWorkflowRef string `json:"jobWorkflowRef"`
|
||||||
|
EventName string `json:"eventName"`
|
||||||
|
RequestLabels []string `json:"requestLabels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Int64List struct {
|
||||||
|
Count int `json:"count"`
|
||||||
|
Value []int64 `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobAvailable struct {
|
||||||
|
AcquireJobUrl string `json:"acquireJobUrl"`
|
||||||
|
JobMessageBase
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobAssigned struct {
|
||||||
|
JobMessageBase
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobStarted struct {
|
||||||
|
RunnerId int `json:"runnerId"`
|
||||||
|
RunnerName string `json:"runnerName"`
|
||||||
|
JobMessageBase
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobCompleted struct {
|
||||||
|
Result string `json:"result"`
|
||||||
|
RunnerId int `json:"runnerId"`
|
||||||
|
RunnerName string `json:"runnerName"`
|
||||||
|
JobMessageBase
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobMessageType struct {
|
||||||
|
MessageType string `json:"messageType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobMessageBase struct {
|
||||||
|
JobMessageType
|
||||||
|
RunnerRequestId int64 `json:"runnerRequestId"`
|
||||||
|
RepositoryName string `json:"repositoryName"`
|
||||||
|
OwnerName string `json:"ownerName"`
|
||||||
|
JobWorkflowRef string `json:"jobWorkflowRef"`
|
||||||
|
JobDisplayName string `json:"jobDisplayName"`
|
||||||
|
WorkflowRunId int64 `json:"workflowRunId"`
|
||||||
|
EventName string `json:"eventName"`
|
||||||
|
RequestLabels []string `json:"requestLabels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Label struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunnerGroup struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
IsDefault bool `json:"isDefaultGroup"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunnerGroupList struct {
|
||||||
|
Count int `json:"count"`
|
||||||
|
RunnerGroups []RunnerGroup `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunnerScaleSet struct {
|
||||||
|
Id int `json:"id,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
RunnerGroupId int `json:"runnerGroupId,omitempty"`
|
||||||
|
RunnerGroupName string `json:"runnerGroupName,omitempty"`
|
||||||
|
Labels []Label `json:"labels,omitempty"`
|
||||||
|
RunnerSetting RunnerSetting `json:"RunnerSetting,omitempty"`
|
||||||
|
CreatedOn time.Time `json:"createdOn,omitempty"`
|
||||||
|
RunnerJitConfigUrl string `json:"runnerJitConfigUrl,omitempty"`
|
||||||
|
Statistics *RunnerScaleSetStatistic `json:"statistics,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunnerScaleSetJitRunnerSetting struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
WorkFolder string `json:"workFolder"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunnerScaleSetMessage struct {
|
||||||
|
MessageId int64 `json:"messageId"`
|
||||||
|
MessageType string `json:"messageType"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
Statistics *RunnerScaleSetStatistic `json:"statistics"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type runnerScaleSetsResponse struct {
|
||||||
|
Count int `json:"count"`
|
||||||
|
RunnerScaleSets []RunnerScaleSet `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunnerScaleSetSession struct {
|
||||||
|
SessionId *uuid.UUID `json:"sessionId,omitempty"`
|
||||||
|
OwnerName string `json:"ownerName,omitempty"`
|
||||||
|
RunnerScaleSet *RunnerScaleSet `json:"runnerScaleSet,omitempty"`
|
||||||
|
MessageQueueUrl string `json:"messageQueueUrl,omitempty"`
|
||||||
|
MessageQueueAccessToken string `json:"messageQueueAccessToken,omitempty"`
|
||||||
|
Statistics *RunnerScaleSetStatistic `json:"statistics,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunnerScaleSetStatistic struct {
|
||||||
|
TotalAvailableJobs int `json:"totalAvailableJobs"`
|
||||||
|
TotalAcquiredJobs int `json:"totalAcquiredJobs"`
|
||||||
|
TotalAssignedJobs int `json:"totalAssignedJobs"`
|
||||||
|
TotalRunningJobs int `json:"totalRunningJobs"`
|
||||||
|
TotalRegisteredRunners int `json:"totalRegisteredRunners"`
|
||||||
|
TotalBusyRunners int `json:"totalBusyRunners"`
|
||||||
|
TotalIdleRunners int `json:"totalIdleRunners"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunnerSetting struct {
|
||||||
|
Ephemeral bool `json:"ephemeral,omitempty"`
|
||||||
|
IsElastic bool `json:"isElastic,omitempty"`
|
||||||
|
DisableUpdate bool `json:"disableUpdate,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunnerReferenceList struct {
|
||||||
|
Count int `json:"count"`
|
||||||
|
RunnerReferences []RunnerReference `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunnerReference struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
RunnerScaleSetId int `json:"runnerScaleSetId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunnerScaleSetJitRunnerConfig struct {
|
||||||
|
Runner *RunnerReference `json:"runner"`
|
||||||
|
EncodedJITConfig string `json:"encodedJITConfig"`
|
||||||
|
}
|
||||||
63
go.mod
63
go.mod
@@ -5,72 +5,93 @@ go 1.19
|
|||||||
require (
|
require (
|
||||||
github.com/bradleyfalzon/ghinstallation/v2 v2.1.0
|
github.com/bradleyfalzon/ghinstallation/v2 v2.1.0
|
||||||
github.com/davecgh/go-spew v1.1.1
|
github.com/davecgh/go-spew v1.1.1
|
||||||
|
github.com/evanphx/json-patch v4.12.0+incompatible
|
||||||
github.com/go-logr/logr v1.2.3
|
github.com/go-logr/logr v1.2.3
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.4.1
|
||||||
github.com/google/go-cmp v0.5.9
|
github.com/google/go-cmp v0.5.9
|
||||||
github.com/google/go-github/v47 v47.1.0
|
github.com/google/go-github/v47 v47.1.0
|
||||||
|
github.com/google/uuid v1.3.0
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79
|
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79
|
||||||
|
github.com/gruntwork-io/terratest v0.40.24
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.1
|
||||||
github.com/kelseyhightower/envconfig v1.4.0
|
github.com/kelseyhightower/envconfig v1.4.0
|
||||||
github.com/onsi/ginkgo v1.16.5
|
github.com/onsi/ginkgo v1.16.5
|
||||||
github.com/onsi/gomega v1.24.1
|
github.com/onsi/gomega v1.20.2
|
||||||
github.com/prometheus/client_golang v1.14.0
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/prometheus/client_golang v1.13.0
|
||||||
|
github.com/stretchr/testify v1.8.0
|
||||||
github.com/teambition/rrule-go v1.8.0
|
github.com/teambition/rrule-go v1.8.0
|
||||||
go.uber.org/zap v1.24.0
|
go.uber.org/multierr v1.7.0
|
||||||
golang.org/x/oauth2 v0.3.0
|
go.uber.org/zap v1.23.0
|
||||||
|
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783
|
||||||
gomodules.xyz/jsonpatch/v2 v2.2.0
|
gomodules.xyz/jsonpatch/v2 v2.2.0
|
||||||
k8s.io/api v0.25.5
|
k8s.io/api v0.25.2
|
||||||
k8s.io/apimachinery v0.25.5
|
k8s.io/apimachinery v0.25.2
|
||||||
k8s.io/client-go v0.25.5
|
k8s.io/client-go v0.25.2
|
||||||
sigs.k8s.io/controller-runtime v0.13.1
|
sigs.k8s.io/controller-runtime v0.13.0
|
||||||
sigs.k8s.io/yaml v1.3.0
|
sigs.k8s.io/yaml v1.3.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go v0.97.0 // indirect
|
cloud.google.com/go/compute v1.12.1 // indirect
|
||||||
|
cloud.google.com/go/compute/metadata v0.2.1 // indirect
|
||||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||||
|
github.com/aws/aws-sdk-go v1.40.56 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||||
github.com/emicklei/go-restful/v3 v3.8.0 // indirect
|
github.com/emicklei/go-restful/v3 v3.8.0 // indirect
|
||||||
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
|
|
||||||
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
|
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||||
|
github.com/ghodss/yaml v1.0.0 // indirect
|
||||||
|
github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0 // indirect
|
||||||
github.com/go-logr/zapr v1.2.3 // indirect
|
github.com/go-logr/zapr v1.2.3 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.19.5 // indirect
|
github.com/go-openapi/jsonreference v0.19.5 // indirect
|
||||||
github.com/go-openapi/swag v0.19.14 // indirect
|
github.com/go-openapi/swag v0.19.14 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.4.1 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang-jwt/jwt/v4 v4.4.1 // indirect
|
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/google/gnostic v0.5.7-v3refs // indirect
|
github.com/google/gnostic v0.5.7-v3refs // indirect
|
||||||
github.com/google/go-github/v45 v45.2.0 // indirect
|
github.com/google/go-github/v45 v45.2.0 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/gofuzz v1.1.0 // indirect
|
github.com/google/gofuzz v1.1.0 // indirect
|
||||||
github.com/google/uuid v1.1.2 // indirect
|
github.com/gruntwork-io/go-commons v0.8.0 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.0 // indirect
|
||||||
github.com/imdario/mergo v0.3.12 // indirect
|
github.com/imdario/mergo v0.3.12 // indirect
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/mailru/easyjson v0.7.6 // indirect
|
github.com/mailru/easyjson v0.7.6 // indirect
|
||||||
|
github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
github.com/moby/spdystream v0.2.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/nxadm/tail v1.4.8 // indirect
|
github.com/nxadm/tail v1.4.8 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.3.0 // indirect
|
github.com/pquerna/otp v1.2.0 // indirect
|
||||||
|
github.com/prometheus/client_model v0.2.0 // indirect
|
||||||
github.com/prometheus/common v0.37.0 // indirect
|
github.com/prometheus/common v0.37.0 // indirect
|
||||||
github.com/prometheus/procfs v0.8.0 // indirect
|
github.com/prometheus/procfs v0.8.0 // indirect
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/stretchr/objx v0.4.0 // indirect
|
||||||
|
github.com/urfave/cli v1.22.2 // indirect
|
||||||
go.uber.org/atomic v1.7.0 // indirect
|
go.uber.org/atomic v1.7.0 // indirect
|
||||||
go.uber.org/multierr v1.6.0 // indirect
|
golang.org/x/crypto v0.0.0-20220824171710-5757bc0c5503 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect
|
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b // indirect
|
||||||
golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 // indirect
|
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
|
||||||
golang.org/x/sys v0.3.0 // indirect
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||||
golang.org/x/term v0.3.0 // indirect
|
golang.org/x/text v0.4.0 // indirect
|
||||||
golang.org/x/text v0.5.0 // indirect
|
|
||||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
|
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/protobuf v1.28.1 // indirect
|
google.golang.org/protobuf v1.28.1 // indirect
|
||||||
|
|||||||
283
go.sum
283
go.sum
@@ -13,25 +13,16 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV
|
|||||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
|
||||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
|
||||||
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
|
|
||||||
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
|
|
||||||
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
|
|
||||||
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
|
|
||||||
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
|
|
||||||
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
|
|
||||||
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
|
|
||||||
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
|
|
||||||
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
|
|
||||||
cloud.google.com/go v0.97.0 h1:3DXvAyifywvq64LfkKaMOmkWPS1CikIQdMe2lY9vxU8=
|
|
||||||
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
|
|
||||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||||
|
cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0=
|
||||||
|
cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
|
||||||
|
cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48=
|
||||||
|
cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
|
||||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||||
@@ -46,7 +37,6 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
|
|||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
|
||||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||||
@@ -58,17 +48,21 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
|
|||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
|
github.com/aws/aws-sdk-go v1.40.56 h1:FM2yjR0UUYFzDTMx+mH9Vyw1k1EUUxsAFzk+BjkzANA=
|
||||||
|
github.com/aws/aws-sdk-go v1.40.56/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||||
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
|
||||||
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
github.com/bradleyfalzon/ghinstallation/v2 v2.1.0 h1:5+NghM1Zred9Z078QEZtm28G/kfDfZN/92gkDlLwGVA=
|
github.com/bradleyfalzon/ghinstallation/v2 v2.1.0 h1:5+NghM1Zred9Z078QEZtm28G/kfDfZN/92gkDlLwGVA=
|
||||||
github.com/bradleyfalzon/ghinstallation/v2 v2.1.0/go.mod h1:Xg3xPRN5Mcq6GDqeUVhFbjEWMb4JHCyWEeeBGEYQoTU=
|
github.com/bradleyfalzon/ghinstallation/v2 v2.1.0/go.mod h1:Xg3xPRN5Mcq6GDqeUVhFbjEWMb4JHCyWEeeBGEYQoTU=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
@@ -77,34 +71,36 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
|
|||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||||
|
github.com/elazarl/goproxy v0.0.0-20190911111923-ecfe977594f1 h1:yY9rWGoXv1U5pl4gxqlULARMQD7x0QG85lqEXTWysik=
|
||||||
github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw=
|
github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw=
|
||||||
github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||||
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
|
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
|
||||||
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||||
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
|
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
|
||||||
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
|
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
|
||||||
|
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
|
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
|
||||||
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
||||||
|
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||||
|
github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0 h1:skJKxRtNmevLqnayafdLe2AsenqRupVmzZSqrvb5caU=
|
||||||
|
github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
@@ -131,6 +127,8 @@ github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE
|
|||||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||||
github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng=
|
github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng=
|
||||||
github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||||
|
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||||
|
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
@@ -151,8 +149,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
|
|||||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||||
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
|
|
||||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
@@ -168,10 +164,8 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
|
|||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
|
||||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54=
|
github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54=
|
||||||
@@ -184,10 +178,8 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
@@ -202,8 +194,6 @@ github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
|
|||||||
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
|
||||||
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
|
|
||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
@@ -211,30 +201,40 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
|
|||||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
|
||||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
|
||||||
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
|
||||||
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
|
||||||
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
|
||||||
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
|
||||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
|
|
||||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/gruntwork-io/go-commons v0.8.0 h1:k/yypwrPqSeYHevLlEDmvmgQzcyTwrlZGRaxEM6G0ro=
|
||||||
|
github.com/gruntwork-io/go-commons v0.8.0/go.mod h1:gtp0yTtIBExIZp7vyIV9I0XQkVwiQZze678hvDXof78=
|
||||||
|
github.com/gruntwork-io/terratest v0.40.24 h1:vxVi714rX+joBLrxBVnbMzSYQ2srIfXzjqvImHl6Rtk=
|
||||||
|
github.com/gruntwork-io/terratest v0.40.24/go.mod h1:JGeIGgLbxbG9/Oqm06z6YXVr76CfomdmLkV564qov+8=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||||
|
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
|
||||||
|
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
|
||||||
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||||
|
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||||
|
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||||
@@ -264,9 +264,19 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN
|
|||||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
|
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||||
|
github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
|
||||||
|
github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326 h1:ofNAzWCcyTALn2Zv40+8XitdzCgXY6e9qvXwN9W0YXg=
|
||||||
|
github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
|
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
|
||||||
|
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -287,30 +297,31 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
|
|||||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||||
github.com/onsi/ginkgo/v2 v2.5.0 h1:TRtrvv2vdQqzkwrQ1ke6vtXf7IK34RBUJafIy1wMwls=
|
github.com/onsi/ginkgo/v2 v2.1.6 h1:Fx2POJZfKRQcM1pH49qSZiYeu319wji004qX+GDovrU=
|
||||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||||
github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E=
|
github.com/onsi/gomega v1.20.2 h1:8uQq0zMgLEfa0vRrrBgaJF2gyW9Da9BmfGV+OyUzfkY=
|
||||||
github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM=
|
github.com/onsi/gomega v1.20.2/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pquerna/otp v1.2.0 h1:/A3+Jn+cagqayeR3iHs/L62m5ue7710D35zl1zJ1kok=
|
||||||
|
github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||||
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||||
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
|
github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU=
|
||||||
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
|
github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
||||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
|
||||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
|
||||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||||
@@ -324,20 +335,22 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
|
|||||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||||
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
||||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
|
||||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
@@ -345,33 +358,31 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
|
|||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/teambition/rrule-go v1.8.0 h1:a/IX5s56hGkFF+nRlJUooZU/45OTeeldBGL29nDKIHw=
|
github.com/teambition/rrule-go v1.8.0 h1:a/IX5s56hGkFF+nRlJUooZU/45OTeeldBGL29nDKIHw=
|
||||||
github.com/teambition/rrule-go v1.8.0/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
|
github.com/teambition/rrule-go v1.8.0/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
|
||||||
|
github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo=
|
||||||
|
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
|
||||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
|
||||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
|
||||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||||
go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
|
go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
|
||||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
|
||||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||||
|
go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
|
||||||
|
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||||
go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
|
go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
|
||||||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
|
||||||
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
@@ -379,8 +390,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38=
|
golang.org/x/crypto v0.0.0-20220824171710-5757bc0c5503 h1:vJ2V3lFLg+bBhgroYuRfyN583UzVveQmIXjc8T/y3to=
|
||||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220824171710-5757bc0c5503/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@@ -403,8 +414,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
|
|||||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
|
||||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
|
||||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
@@ -413,9 +422,6 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
|||||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -448,37 +454,22 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
|
|||||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
|
||||||
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 h1:Frnccbp+ok2GkUS2tC84yAq/U9Vg+0sIO7aRL3T4Xnc=
|
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU=
|
||||||
golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||||
golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8=
|
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk=
|
||||||
golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
|
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -489,12 +480,12 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -506,6 +497,7 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -525,50 +517,30 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
|
||||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
|
|
||||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
|
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
@@ -616,19 +588,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
|
|||||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
|
||||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
|
||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
|
||||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
|
||||||
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
|
||||||
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
|
||||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -651,18 +612,6 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
|
|||||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
|
||||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
|
||||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
|
||||||
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
|
|
||||||
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
|
|
||||||
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
|
|
||||||
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
|
|
||||||
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
|
|
||||||
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
|
|
||||||
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
|
|
||||||
google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
|
|
||||||
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
|
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
@@ -694,39 +643,13 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG
|
|||||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
|
||||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
|
|
||||||
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
|
|
||||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
|
||||||
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
|
||||||
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
|
||||||
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
|
|
||||||
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
|
|
||||||
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
|
|
||||||
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
|
||||||
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
|
||||||
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
|
|
||||||
google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
|
||||||
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
|
||||||
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
|
||||||
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
|
||||||
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
@@ -739,20 +662,6 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
|
|||||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
|
||||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
|
||||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
|
||||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
|
||||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
|
||||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
|
||||||
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
|
||||||
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
|
||||||
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
|
||||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
|
||||||
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
|
||||||
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
|
||||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
|
||||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
@@ -765,7 +674,6 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
|
|||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
|
||||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
@@ -782,7 +690,6 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep
|
|||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
@@ -801,14 +708,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
|||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
k8s.io/api v0.25.5 h1:mqyHf7aoaYMpdvO87mqpol+Qnsmo+y09S0PMIXwiZKo=
|
k8s.io/api v0.25.2 h1:v6G8RyFcwf0HR5jQGIAYlvtRNrxMJQG1xJzaSeVnIS8=
|
||||||
k8s.io/api v0.25.5/go.mod h1:RzplZX0Z8rV/WhSTfEvnyd91bBhBQTRWo85qBQwRmb8=
|
k8s.io/api v0.25.2/go.mod h1:qP1Rn4sCVFwx/xIhe+we2cwBLTXNcheRyYXwajonhy0=
|
||||||
k8s.io/apiextensions-apiserver v0.25.0 h1:CJ9zlyXAbq0FIW8CD7HHyozCMBpDSiH7EdrSTCZcZFY=
|
k8s.io/apiextensions-apiserver v0.25.0 h1:CJ9zlyXAbq0FIW8CD7HHyozCMBpDSiH7EdrSTCZcZFY=
|
||||||
k8s.io/apiextensions-apiserver v0.25.0/go.mod h1:3pAjZiN4zw7R8aZC5gR0y3/vCkGlAjCazcg1me8iB/E=
|
k8s.io/apiextensions-apiserver v0.25.0/go.mod h1:3pAjZiN4zw7R8aZC5gR0y3/vCkGlAjCazcg1me8iB/E=
|
||||||
k8s.io/apimachinery v0.25.5 h1:SQomYHvv+aO43qdu3QKRf9YuI0oI8w3RrOQ1qPbAUGY=
|
k8s.io/apimachinery v0.25.2 h1:WbxfAjCx+AeN8Ilp9joWnyJ6xu9OMeS/fsfjK/5zaQs=
|
||||||
k8s.io/apimachinery v0.25.5/go.mod h1:1S2i1QHkmxc8+EZCIxe/fX5hpldVXk4gvnJInMEb8D4=
|
k8s.io/apimachinery v0.25.2/go.mod h1:hqqA1X0bsgsxI6dXsJ4HnNTBOmJNxyPp8dw3u2fSHwA=
|
||||||
k8s.io/client-go v0.25.5 h1:7QWVK0Ph4bLn0UwotPTc2FTgm8shreQXyvXnnHDd8rE=
|
k8s.io/client-go v0.25.2 h1:SUPp9p5CwM0yXGQrwYurw9LWz+YtMwhWd0GqOsSiefo=
|
||||||
k8s.io/client-go v0.25.5/go.mod h1:bOeoaUUdpyz3WDFGo+Xm3nOQFh2KuYXRDwrvbAPtFQA=
|
k8s.io/client-go v0.25.2/go.mod h1:i7cNU7N+yGQmJkewcRD2+Vuj4iz7b30kI8OcL3horQ4=
|
||||||
k8s.io/component-base v0.25.0 h1:haVKlLkPCFZhkcqB6WCvpVxftrg6+FK5x1ZuaIDaQ5Y=
|
k8s.io/component-base v0.25.0 h1:haVKlLkPCFZhkcqB6WCvpVxftrg6+FK5x1ZuaIDaQ5Y=
|
||||||
k8s.io/component-base v0.25.0/go.mod h1:F2Sumv9CnbBlqrpdf7rKZTmmd2meJq0HizeyY/yAFxk=
|
k8s.io/component-base v0.25.0/go.mod h1:F2Sumv9CnbBlqrpdf7rKZTmmd2meJq0HizeyY/yAFxk=
|
||||||
k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
|
k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
|
||||||
@@ -821,8 +728,8 @@ k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/
|
|||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||||
sigs.k8s.io/controller-runtime v0.13.1 h1:tUsRCSJVM1QQOOeViGeX3GMT3dQF1eePPw6sEE3xSlg=
|
sigs.k8s.io/controller-runtime v0.13.0 h1:iqa5RNciy7ADWnIc8QxCbOX5FEKVR3uxVxKHRMc2WIQ=
|
||||||
sigs.k8s.io/controller-runtime v0.13.1/go.mod h1:Zbz+el8Yg31jubvAEyglRZGdLAjplZl+PgtYNI6WNTI=
|
sigs.k8s.io/controller-runtime v0.13.0/go.mod h1:Zbz+el8Yg31jubvAEyglRZGdLAjplZl+PgtYNI6WNTI=
|
||||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k=
|
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k=
|
||||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
|
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package hash
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/fnv"
|
"hash/fnv"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/util/rand"
|
"k8s.io/apimachinery/pkg/util/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,3 +16,9 @@ func FNVHashStringObjects(objs ...interface{}) string {
|
|||||||
|
|
||||||
return rand.SafeEncodeString(fmt.Sprint(hash.Sum32()))
|
return rand.SafeEncodeString(fmt.Sprint(hash.Sum32()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FNVHashString(name string) string {
|
||||||
|
hash := fnv.New32a()
|
||||||
|
hash.Write([]byte(name))
|
||||||
|
return rand.SafeEncodeString(fmt.Sprint(hash.Sum32()))
|
||||||
|
}
|
||||||
|
|||||||
26
hash/hash.go
26
hash/hash.go
@@ -5,9 +5,12 @@
|
|||||||
package hash
|
package hash
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
|
"hash/fnv"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"k8s.io/apimachinery/pkg/util/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DeepHashObject writes specified object to hash using the spew library
|
// DeepHashObject writes specified object to hash using the spew library
|
||||||
@@ -23,3 +26,26 @@ func DeepHashObject(hasher hash.Hash, objectToWrite interface{}) {
|
|||||||
}
|
}
|
||||||
printer.Fprintf(hasher, "%#v", objectToWrite)
|
printer.Fprintf(hasher, "%#v", objectToWrite)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ComputeHash returns a hash value calculated from template and
|
||||||
|
// a collisionCount to avoid hash collision. The hash will be safe encoded to
|
||||||
|
// avoid bad words. It expects **template. In other words, you should pass an address
|
||||||
|
// of a DeepCopy result.
|
||||||
|
//
|
||||||
|
// Proudly modified and adopted from k8s.io/kubernetes/pkg/util/hash.DeepHashObject and
|
||||||
|
// k8s.io/kubernetes/pkg/controller.ComputeHash.
|
||||||
|
func ComputeTemplateHash(template interface{}) string {
|
||||||
|
hasher := fnv.New32a()
|
||||||
|
|
||||||
|
hasher.Reset()
|
||||||
|
|
||||||
|
printer := spew.ConfigState{
|
||||||
|
Indent: " ",
|
||||||
|
SortKeys: true,
|
||||||
|
DisableMethods: true,
|
||||||
|
SpewKeys: true,
|
||||||
|
}
|
||||||
|
printer.Fprintf(hasher, "%#v", template)
|
||||||
|
|
||||||
|
return rand.SafeEncodeString(fmt.Sprint(hasher.Sum32()))
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ const (
|
|||||||
LogLevelInfo = "info"
|
LogLevelInfo = "info"
|
||||||
LogLevelWarn = "warn"
|
LogLevelWarn = "warn"
|
||||||
LogLevelError = "error"
|
LogLevelError = "error"
|
||||||
|
LogFormatText = "text"
|
||||||
|
LogFormatJSON = "json"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
111
main.go
111
main.go
@@ -17,20 +17,25 @@ limitations under the License.
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
actionsv1alpha1 "github.com/actions/actions-runner-controller/apis/actions.summerwind.net/v1alpha1"
|
githubv1alpha1 "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
|
||||||
|
summerwindv1alpha1 "github.com/actions/actions-runner-controller/apis/actions.summerwind.net/v1alpha1"
|
||||||
"github.com/actions/actions-runner-controller/build"
|
"github.com/actions/actions-runner-controller/build"
|
||||||
|
actionsgithubcom "github.com/actions/actions-runner-controller/controllers/actions.github.com"
|
||||||
actionssummerwindnet "github.com/actions/actions-runner-controller/controllers/actions.summerwind.net"
|
actionssummerwindnet "github.com/actions/actions-runner-controller/controllers/actions.summerwind.net"
|
||||||
"github.com/actions/actions-runner-controller/github"
|
"github.com/actions/actions-runner-controller/github"
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
"github.com/actions/actions-runner-controller/logging"
|
"github.com/actions/actions-runner-controller/logging"
|
||||||
"github.com/kelseyhightower/envconfig"
|
"github.com/kelseyhightower/envconfig"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||||
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
@@ -48,8 +53,8 @@ var (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
_ = clientgoscheme.AddToScheme(scheme)
|
_ = clientgoscheme.AddToScheme(scheme)
|
||||||
|
_ = githubv1alpha1.AddToScheme(scheme)
|
||||||
_ = actionsv1alpha1.AddToScheme(scheme)
|
_ = summerwindv1alpha1.AddToScheme(scheme)
|
||||||
// +kubebuilder:scaffold:scheme
|
// +kubebuilder:scaffold:scheme
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +74,9 @@ func main() {
|
|||||||
ghClient *github.Client
|
ghClient *github.Client
|
||||||
|
|
||||||
metricsAddr string
|
metricsAddr string
|
||||||
|
autoScalingRunnerSetOnly bool
|
||||||
enableLeaderElection bool
|
enableLeaderElection bool
|
||||||
|
disableAdmissionWebhook bool
|
||||||
runnerStatusUpdateHook bool
|
runnerStatusUpdateHook bool
|
||||||
leaderElectionId string
|
leaderElectionId string
|
||||||
port int
|
port int
|
||||||
@@ -86,6 +93,8 @@ func main() {
|
|||||||
logLevel string
|
logLevel string
|
||||||
logFormat string
|
logFormat string
|
||||||
|
|
||||||
|
autoScalerImagePullSecrets stringSlice
|
||||||
|
|
||||||
commonRunnerLabels commaSeparatedStringSlice
|
commonRunnerLabels commaSeparatedStringSlice
|
||||||
)
|
)
|
||||||
var c github.Config
|
var c github.Config
|
||||||
@@ -121,7 +130,8 @@ func main() {
|
|||||||
flag.StringVar(&namespace, "watch-namespace", "", "The namespace to watch for custom resources. Set to empty for letting it watch for all namespaces.")
|
flag.StringVar(&namespace, "watch-namespace", "", "The namespace to watch for custom resources. Set to empty for letting it watch for all namespaces.")
|
||||||
flag.StringVar(&logLevel, "log-level", logging.LogLevelDebug, `The verbosity of the logging. Valid values are "debug", "info", "warn", "error". Defaults to "debug".`)
|
flag.StringVar(&logLevel, "log-level", logging.LogLevelDebug, `The verbosity of the logging. Valid values are "debug", "info", "warn", "error". Defaults to "debug".`)
|
||||||
flag.StringVar(&logFormat, "log-format", "text", `The log format. Valid options are "text" and "json". Defaults to "text"`)
|
flag.StringVar(&logFormat, "log-format", "text", `The log format. Valid options are "text" and "json". Defaults to "text"`)
|
||||||
|
flag.BoolVar(&autoScalingRunnerSetOnly, "auto-scaling-runner-set-only", false, "Make controller only reconcile AutoRunnerScaleSet object.")
|
||||||
|
flag.Var(&autoScalerImagePullSecrets, "auto-scaler-image-pull-secrets", "The default image-pull secret name for auto-scaler listener container.")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
log, err := logging.NewLogger(logLevel, logFormat)
|
log, err := logging.NewLogger(logLevel, logFormat)
|
||||||
@@ -131,14 +141,21 @@ func main() {
|
|||||||
}
|
}
|
||||||
c.Log = &log
|
c.Log = &log
|
||||||
|
|
||||||
|
if !autoScalingRunnerSetOnly {
|
||||||
ghClient, err = c.NewClient()
|
ghClient, err = c.NewClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "Error: Client creation failed.", err)
|
log.Error(err, "unable to create client")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctrl.SetLogger(log)
|
ctrl.SetLogger(log)
|
||||||
|
|
||||||
|
if autoScalingRunnerSetOnly {
|
||||||
|
// We don't support metrics for AutoRunnerScaleSet for now
|
||||||
|
metricsAddr = "0"
|
||||||
|
}
|
||||||
|
|
||||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||||
Scheme: scheme,
|
Scheme: scheme,
|
||||||
MetricsBindAddress: metricsAddr,
|
MetricsBindAddress: metricsAddr,
|
||||||
@@ -158,6 +175,12 @@ func main() {
|
|||||||
ghClient,
|
ghClient,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
actionsMultiClient := actions.NewMultiClient(
|
||||||
|
"actions-runner-controller/"+build.Version,
|
||||||
|
log.WithName("actions-clients"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if !autoScalingRunnerSetOnly {
|
||||||
runnerReconciler := &actionssummerwindnet.RunnerReconciler{
|
runnerReconciler := &actionssummerwindnet.RunnerReconciler{
|
||||||
Client: mgr.GetClient(),
|
Client: mgr.GetClient(),
|
||||||
Log: log.WithName("runner"),
|
Log: log.WithName("runner"),
|
||||||
@@ -278,20 +301,89 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = (&actionsv1alpha1.Runner{}).SetupWebhookWithManager(mgr); err != nil {
|
if !disableAdmissionWebhook {
|
||||||
|
if err = (&summerwindv1alpha1.Runner{}).SetupWebhookWithManager(mgr); err != nil {
|
||||||
log.Error(err, "unable to create webhook", "webhook", "Runner")
|
log.Error(err, "unable to create webhook", "webhook", "Runner")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if err = (&actionsv1alpha1.RunnerDeployment{}).SetupWebhookWithManager(mgr); err != nil {
|
if err = (&summerwindv1alpha1.RunnerDeployment{}).SetupWebhookWithManager(mgr); err != nil {
|
||||||
log.Error(err, "unable to create webhook", "webhook", "RunnerDeployment")
|
log.Error(err, "unable to create webhook", "webhook", "RunnerDeployment")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if err = (&actionsv1alpha1.RunnerReplicaSet{}).SetupWebhookWithManager(mgr); err != nil {
|
if err = (&summerwindv1alpha1.RunnerReplicaSet{}).SetupWebhookWithManager(mgr); err != nil {
|
||||||
log.Error(err, "unable to create webhook", "webhook", "RunnerReplicaSet")
|
log.Error(err, "unable to create webhook", "webhook", "RunnerReplicaSet")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mgrPodName := os.Getenv("CONTROLLER_MANAGER_POD_NAME")
|
||||||
|
mgrPodNamespace := os.Getenv("CONTROLLER_MANAGER_POD_NAMESPACE")
|
||||||
|
var mgrPod corev1.Pod
|
||||||
|
err = mgr.GetAPIReader().Get(context.Background(), types.NamespacedName{Namespace: mgrPodNamespace, Name: mgrPodName}, &mgrPod)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err, fmt.Sprintf("unable to obtain manager pod: %s (%s)", mgrPodName, mgrPodNamespace))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var mgrContainer *corev1.Container
|
||||||
|
for _, container := range mgrPod.Spec.Containers {
|
||||||
|
if container.Name == "manager" {
|
||||||
|
mgrContainer = &container
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mgrContainer != nil {
|
||||||
|
log.Info("Detected manager container", "image", mgrContainer.Image)
|
||||||
|
} else {
|
||||||
|
log.Error(err, "unable to obtain manager container image")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = (&actionsgithubcom.AutoscalingRunnerSetReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Log: log.WithName("AutoscalingRunnerSet"),
|
||||||
|
Scheme: mgr.GetScheme(),
|
||||||
|
ControllerNamespace: mgrPodNamespace,
|
||||||
|
DefaultRunnerScaleSetListenerImage: mgrContainer.Image,
|
||||||
|
ActionsClient: actionsMultiClient,
|
||||||
|
DefaultRunnerScaleSetListenerImagePullSecrets: autoScalerImagePullSecrets,
|
||||||
|
}).SetupWithManager(mgr); err != nil {
|
||||||
|
log.Error(err, "unable to create controller", "controller", "AutoscalingRunnerSet")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = (&actionsgithubcom.EphemeralRunnerReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Log: log.WithName("EphemeralRunner"),
|
||||||
|
Scheme: mgr.GetScheme(),
|
||||||
|
ActionsClient: actionsMultiClient,
|
||||||
|
}).SetupWithManager(mgr); err != nil {
|
||||||
|
log.Error(err, "unable to create controller", "controller", "EphemeralRunner")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = (&actionsgithubcom.EphemeralRunnerSetReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Log: log.WithName("EphemeralRunnerSet"),
|
||||||
|
Scheme: mgr.GetScheme(),
|
||||||
|
ActionsClient: actionsMultiClient,
|
||||||
|
}).SetupWithManager(mgr); err != nil {
|
||||||
|
log.Error(err, "unable to create controller", "controller", "EphemeralRunnerSet")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err = (&actionsgithubcom.AutoscalingListenerReconciler{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Log: log.WithName("AutoscalingListener"),
|
||||||
|
Scheme: mgr.GetScheme(),
|
||||||
|
}).SetupWithManager(mgr); err != nil {
|
||||||
|
log.Error(err, "unable to create controller", "controller", "AutoscalingListener")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
// +kubebuilder:scaffold:builder
|
// +kubebuilder:scaffold:builder
|
||||||
|
|
||||||
|
if !disableAdmissionWebhook && !autoScalingRunnerSetOnly {
|
||||||
injector := &actionssummerwindnet.PodRunnerTokenInjector{
|
injector := &actionssummerwindnet.PodRunnerTokenInjector{
|
||||||
Client: mgr.GetClient(),
|
Client: mgr.GetClient(),
|
||||||
GitHubClient: multiClient,
|
GitHubClient: multiClient,
|
||||||
@@ -301,6 +393,7 @@ func main() {
|
|||||||
log.Error(err, "unable to create webhook server", "webhook", "PodRunnerTokenInjector")
|
log.Error(err, "unable to create webhook server", "webhook", "PodRunnerTokenInjector")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log.Info("starting manager")
|
log.Info("starting manager")
|
||||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user