Skip to content

Commit

Permalink
feat: support distributed tracing
Browse files Browse the repository at this point in the history
Signed-off-by: namkyu1999 <lak9348@konkuk.ac.kr>
  • Loading branch information
namkyu1999 committed Jul 5, 2024
1 parent e96a7ee commit 9d38a70
Show file tree
Hide file tree
Showing 8 changed files with 1,657 additions and 284 deletions.
51 changes: 30 additions & 21 deletions controllers/chaosengine_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,23 @@ package controllers
import (
"context"
"fmt"
"os"
"reflect"
"strings"
"time"

"github.com/go-logr/logr"
litmuschaosv1alpha1 "github.com/litmuschaos/chaos-operator/api/litmuschaos/v1alpha1"
"github.com/litmuschaos/chaos-operator/pkg/analytics"
dynamicclientset "github.com/litmuschaos/chaos-operator/pkg/client/dynamic"
"github.com/litmuschaos/chaos-operator/pkg/telemetry"
chaosTypes "github.com/litmuschaos/chaos-operator/pkg/types"
"github.com/litmuschaos/chaos-operator/pkg/utils"
"github.com/litmuschaos/chaos-operator/pkg/utils/retry"
"github.com/litmuschaos/elves/kubernetes/container"
"github.com/litmuschaos/elves/kubernetes/pod"
"github.com/pkg/errors"
"go.opentelemetry.io/otel"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
Expand All @@ -37,14 +44,10 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"os"
"reflect"
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/reconcile"
"strings"
"time"
)

const finalizer = "chaosengine.litmuschaos.io/finalizer"
Expand Down Expand Up @@ -96,6 +99,8 @@ func (r *ChaosEngineReconciler) Reconcile(ctx context.Context, request ctrl.Requ
return reconcile.Result{}, err
}

ctx = telemetry.InitSpanContext(ctx, engine.Instance)

// Handle deletion of ChaosEngine
if engine.Instance.ObjectMeta.GetDeletionTimestamp() != nil {
return r.reconcileForDelete(engine, request)
Expand All @@ -111,7 +116,7 @@ func (r *ChaosEngineReconciler) Reconcile(ctx context.Context, request ctrl.Requ

// Handling of normal execution of ChaosEngine
if engine.Instance.Spec.EngineState == litmuschaosv1alpha1.EngineStateActive && engine.Instance.Status.EngineStatus == litmuschaosv1alpha1.EngineStatusInitialized {
return r.reconcileForCreationAndRunning(engine, reqLogger)
return r.reconcileForCreationAndRunning(ctx, engine, reqLogger)
}

// Handling Graceful completion of ChaosEngine
Expand All @@ -138,7 +143,7 @@ func (r *ChaosEngineReconciler) Reconcile(ctx context.Context, request ctrl.Requ
}

// getChaosRunnerENV return the env required for chaos-runner
func getChaosRunnerENV(engine *chaosTypes.EngineInfo, ClientUUID string) []corev1.EnvVar {
func getChaosRunnerENV(ctx context.Context, engine *chaosTypes.EngineInfo, ClientUUID string) []corev1.EnvVar {

var envDetails utils.ENVDetails
envDetails.SetEnv("CHAOSENGINE", engine.Instance.Name).
Expand All @@ -147,7 +152,9 @@ func getChaosRunnerENV(engine *chaosTypes.EngineInfo, ClientUUID string) []corev
SetEnv("CHAOS_SVC_ACC", engine.Instance.Spec.ChaosServiceAccount).
SetEnv("AUXILIARY_APPINFO", engine.Instance.Spec.AuxiliaryAppInfo).
SetEnv("CLIENT_UUID", ClientUUID).
SetEnv("CHAOS_NAMESPACE", engine.Instance.Namespace)
SetEnv("CHAOS_NAMESPACE", engine.Instance.Namespace).
SetEnv("OTEL_EXPORTER_OTLP_ENDPOINT", os.Getenv(telemetry.OTELExporterOTLPEndpoint)).
SetEnv("TRACE_PARENT", telemetry.GetMarshalledSpanFromContext(ctx))

return envDetails.ENV
}
Expand All @@ -167,7 +174,7 @@ func getChaosRunnerLabels(cr *litmuschaosv1alpha1.ChaosEngine) map[string]string
}

// newGoRunnerPodForCR defines a new go-based Runner Pod
func (r *ChaosEngineReconciler) newGoRunnerPodForCR(engine *chaosTypes.EngineInfo) (*corev1.Pod, error) {
func (r *ChaosEngineReconciler) newGoRunnerPodForCR(ctx context.Context, engine *chaosTypes.EngineInfo) (*corev1.Pod, error) {
var experiment litmuschaosv1alpha1.ChaosExperiment
if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: engine.Instance.Spec.Experiments[0].Name, Namespace: engine.Instance.Namespace}, &experiment); err != nil {
return nil, err
Expand All @@ -176,7 +183,7 @@ func (r *ChaosEngineReconciler) newGoRunnerPodForCR(engine *chaosTypes.EngineInf
engine.VolumeOpts.VolumeOperations(engine.Instance.Spec.Components.Runner.ConfigMaps, engine.Instance.Spec.Components.Runner.Secrets)

containerForRunner := container.NewBuilder().
WithEnvsNew(getChaosRunnerENV(engine, analytics.ClientUUID)).
WithEnvsNew(getChaosRunnerENV(ctx, engine, analytics.ClientUUID)).
WithName("chaos-runner").
WithImage(engine.Instance.Spec.Components.Runner.Image).
WithImagePullPolicy(corev1.PullIfNotPresent)
Expand Down Expand Up @@ -245,10 +252,12 @@ func (r *ChaosEngineReconciler) newGoRunnerPodForCR(engine *chaosTypes.EngineInf
}

// engineRunnerPod to Check if the engineRunner pod already exists, else create
func engineRunnerPod(runnerPod *podEngineRunner) error {
if err := runnerPod.r.Client.Get(context.TODO(), types.NamespacedName{Name: runnerPod.engineRunner.Name, Namespace: runnerPod.engineRunner.Namespace}, runnerPod.pod); err != nil && k8serrors.IsNotFound(err) {
func engineRunnerPod(ctx context.Context, runnerPod *podEngineRunner) error {
if err := runnerPod.r.Client.Get(ctx, types.NamespacedName{Name: runnerPod.engineRunner.Name, Namespace: runnerPod.engineRunner.Namespace}, runnerPod.pod); err != nil && k8serrors.IsNotFound(err) {
ctx, span := otel.Tracer(telemetry.TracerName).Start(ctx, "createChaosRunnerPod")
defer span.End()
runnerPod.reqLogger.Info("Creating a new engineRunner Pod", "Pod.Namespace", runnerPod.engineRunner.Namespace, "Pod.Name", runnerPod.engineRunner.Name)
if err = runnerPod.r.Client.Create(context.TODO(), runnerPod.engineRunner); err != nil {
if err = runnerPod.r.Client.Create(ctx, runnerPod.engineRunner); err != nil {
if k8serrors.IsAlreadyExists(err) {
runnerPod.reqLogger.Info("Skip reconcile: engineRunner Pod already exists", "Pod.Namespace", runnerPod.pod.Namespace, "Pod.Name", runnerPod.pod.Name)
return nil
Expand Down Expand Up @@ -280,12 +289,12 @@ func (r *ChaosEngineReconciler) getChaosEngineInstance(engine *chaosTypes.Engine
}

// Check if the engineRunner pod already exists, else create
func (r *ChaosEngineReconciler) checkEngineRunnerPod(engine *chaosTypes.EngineInfo, reqLogger logr.Logger) error {
func (r *ChaosEngineReconciler) checkEngineRunnerPod(ctx context.Context, engine *chaosTypes.EngineInfo, reqLogger logr.Logger) error {
if len(engine.AppExperiments) == 0 {
return errors.New("application experiment list is empty")
}

engineRunner, err := r.newGoRunnerPodForCR(engine)
engineRunner, err := r.newGoRunnerPodForCR(ctx, engine)
if err != nil {
return err
}
Expand All @@ -302,7 +311,7 @@ func (r *ChaosEngineReconciler) checkEngineRunnerPod(engine *chaosTypes.EngineIn
reconcileEngine: engineReconcile,
}

return engineRunnerPod(runnerPod)
return engineRunnerPod(ctx, runnerPod)
}

// setChaosResourceImage take the runner image from engine spec
Expand Down Expand Up @@ -552,11 +561,11 @@ func (r *ChaosEngineReconciler) initEngine(engine *chaosTypes.EngineInfo) (bool,
}

// reconcileForCreationAndRunning reconciles for Chaos execution of Chaos Engine
func (r *ChaosEngineReconciler) reconcileForCreationAndRunning(engine *chaosTypes.EngineInfo, reqLogger logr.Logger) (reconcile.Result, error) {
func (r *ChaosEngineReconciler) reconcileForCreationAndRunning(ctx context.Context, engine *chaosTypes.EngineInfo, reqLogger logr.Logger) (reconcile.Result, error) {
var runner corev1.Pod
if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: engine.Instance.Name + "-runner", Namespace: engine.Instance.Namespace}, &runner); err != nil {
if err := r.Client.Get(ctx, types.NamespacedName{Name: engine.Instance.Name + "-runner", Namespace: engine.Instance.Namespace}, &runner); err != nil {
if k8serrors.IsNotFound(err) {
return r.createRunnerPod(engine, reqLogger)
return r.createRunnerPod(ctx, engine, reqLogger)
}
return reconcile.Result{}, err
}
Expand Down Expand Up @@ -585,7 +594,7 @@ func (r *ChaosEngineReconciler) reconcileForCreationAndRunning(engine *chaosType
return reconcile.Result{}, nil
}

func (r *ChaosEngineReconciler) createRunnerPod(engine *chaosTypes.EngineInfo, reqLogger logr.Logger) (reconcile.Result, error) {
func (r *ChaosEngineReconciler) createRunnerPod(ctx context.Context, engine *chaosTypes.EngineInfo, reqLogger logr.Logger) (reconcile.Result, error) {
if err := r.setExperimentDetails(engine); err != nil {
if updateEngineErr := r.updateEngineState(engine, litmuschaosv1alpha1.EngineStateStop); updateEngineErr != nil {
r.Recorder.Eventf(engine.Instance, corev1.EventTypeWarning, "ChaosResourcesOperationFailed", "(chaos stop) Unable to update chaosengine")
Expand All @@ -595,7 +604,7 @@ func (r *ChaosEngineReconciler) createRunnerPod(engine *chaosTypes.EngineInfo, r
}

// Check if the engineRunner pod already exists, else create
if err := r.checkEngineRunnerPod(engine, reqLogger); err != nil {
if err := r.checkEngineRunnerPod(ctx, engine, reqLogger); err != nil {
r.Recorder.Eventf(engine.Instance, corev1.EventTypeWarning, "ChaosResourcesOperationFailed", "(chaos start) Unable to get chaos resources")
return reconcile.Result{}, err
}
Expand Down Expand Up @@ -826,7 +835,7 @@ func isResultCRDAvailable() (bool, error) {
Resource: "customresourcedefinitions",
}

resultList, err := (*dynamicClient).Resource(gvr).List(context.Background(), v1.ListOptions{})
resultList, err := (dynamicClient).Resource(gvr).List(context.Background(), v1.ListOptions{})
if err != nil {
return false, err
}
Expand Down
179 changes: 88 additions & 91 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,119 +1,116 @@
module github.com/litmuschaos/chaos-operator

go 1.20

require (
cloud.google.com/go v0.81.0 // indirect
github.com/go-logr/logr v1.2.3
github.com/google/go-cmp v0.5.6 // indirect
github.com/jpillora/go-ogle-analytics v0.0.0-20161213085824-14b04e0594ef
github.com/litmuschaos/elves v0.0.0-20230607095010-c7119636b529
github.com/pkg/errors v0.9.1
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/crypto v0.16.0 // indirect
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
k8s.io/api v0.26.0
k8s.io/apimachinery v0.26.0
k8s.io/client-go v12.0.0+incompatible
sigs.k8s.io/controller-runtime v0.10.0
)
go 1.22.2

require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24
github.com/go-logr/logr v1.4.2
github.com/google/martian v2.1.0+incompatible
github.com/onsi/ginkgo v1.16.4
github.com/onsi/gomega v1.15.0
github.com/jpillora/go-ogle-analytics v0.0.0-20161213085824-14b04e0594ef
github.com/litmuschaos/elves v0.0.0-20240620125031-6b51de2a7d0b
github.com/onsi/ginkgo v1.16.5
github.com/onsi/gomega v1.33.1
github.com/operator-framework/operator-sdk v0.19.0
github.com/stretchr/testify v1.7.0
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.9.0
go.opentelemetry.io/otel v1.27.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0
go.opentelemetry.io/otel/sdk v1.27.0
k8s.io/api v0.30.2
k8s.io/apimachinery v0.30.2
k8s.io/client-go v12.0.0+incompatible
k8s.io/klog v1.0.0
sigs.k8s.io/controller-runtime v0.14.6
)

require (
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.18 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/evanphx/json-patch v4.11.0+incompatible // indirect
github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/go-logr/zapr v0.4.0 // indirect
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/google/uuid v1.1.2 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/hashicorp/golang-lru v0.5.3 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic v0.7.0 // indirect
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.12.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.19.0 // indirect
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/term v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
golang.org/x/tools v0.16.1 // indirect
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.26.0 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.54.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.opentelemetry.io/otel/metric v1.27.0 // indirect
go.opentelemetry.io/otel/trace v1.27.0 // indirect
go.opentelemetry.io/proto/otlp v1.2.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/term v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.22.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect
google.golang.org/grpc v1.64.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.22.1 // indirect
k8s.io/component-base v0.22.2 // indirect
k8s.io/klog/v2 v2.80.1 // indirect
k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e // indirect
k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect
k8s.io/apiextensions-apiserver v0.30.2 // indirect
k8s.io/component-base v0.26.15 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20240620174524-b456828f718b // indirect
k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

// Pinned to kubernetes-1.21.2
// Pinned to kubernetes-1.26
replace (
github.com/go-logr/logr => github.com/go-logr/logr v0.4.0
k8s.io/api => k8s.io/api v0.21.2
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.21.2
k8s.io/apimachinery => k8s.io/apimachinery v0.21.2
k8s.io/apiserver => k8s.io/apiserver v0.21.2
k8s.io/cli-runtime => k8s.io/cli-runtime v0.21.2
k8s.io/client-go => k8s.io/client-go v0.21.2
k8s.io/cloud-provider => k8s.io/cloud-provider v0.21.2
k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.21.2
k8s.io/code-generator => k8s.io/code-generator v0.21.2
k8s.io/component-base => k8s.io/component-base v0.21.2
k8s.io/cri-api => k8s.io/cri-api v0.21.2
k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.21.2
k8s.io/klog/v2 => k8s.io/klog/v2 v2.9.0
k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.21.2
k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.21.2
k8s.io/kube-proxy => k8s.io/kube-proxy v0.21.2
k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.21.2
k8s.io/kubectl => k8s.io/kubectl v0.21.2
k8s.io/kubelet => k8s.io/kubelet v0.21.2
k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.21.2
k8s.io/metrics => k8s.io/metrics v0.21.2
k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.21.2
github.com/go-logr/logr => github.com/go-logr/logr v1.4.2
k8s.io/api => k8s.io/api v0.26.15
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.26.15
k8s.io/apimachinery => k8s.io/apimachinery v0.26.15
k8s.io/client-go => k8s.io/client-go v0.26.15
k8s.io/cloud-provider => k8s.io/cloud-provider v0.26.15
k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.26.15
k8s.io/component-base => k8s.io/component-base v0.26.15
k8s.io/cri-api => k8s.io/cri-api v0.26.15
k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.26.15
k8s.io/klog/v2 => k8s.io/klog/v2 v2.80.1
k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.26.15
k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.26.15
k8s.io/kube-proxy => k8s.io/kube-proxy v0.26.15
k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.26.15
k8s.io/kubelet => k8s.io/kubelet v0.26.15
k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.26.15
k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.26.15
)

replace github.com/docker/docker => github.com/moby/moby v0.7.3-0.20190826074503-38ab9da00309 // Required by Helm

replace github.com/Azure/go-autorest => github.com/Azure/go-autorest v14.2.0+incompatible
Loading

0 comments on commit 9d38a70

Please sign in to comment.