diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d0d6fde125..77cfcac1ca 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,4 +1,5 @@ # est-harshname: product-catalog-ci +#testing the github actions on: pull_request: @@ -12,6 +13,16 @@ jobs: - name: checkout code uses: actions/checkout@v4 + - name: Cache Go modules + uses: actions/cache@v3 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('src/product-catalog/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Setup Go 1.22 uses: actions/setup-go@v5 # Using a more recent version with: diff --git a/src/product-catalog/main.go b/src/product-catalog/main.go index dc48cec9ca..d0d6fde125 100644 --- a/src/product-catalog/main.go +++ b/src/product-catalog/main.go @@ -1,346 +1,95 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 -package main - -//go:generate go install google.golang.org/protobuf/cmd/protoc-gen-go -//go:generate go install google.golang.org/grpc/cmd/protoc-gen-go-grpc -//go:generate protoc --go_out=./ --go-grpc_out=./ --proto_path=../../pb ../../pb/demo.proto - -import ( - "context" - "fmt" - "io/fs" - "net" - "os" - "os/signal" - "strings" - "sync" - "syscall" - "time" - - "github.com/sirupsen/logrus" - - "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" - "go.opentelemetry.io/contrib/instrumentation/runtime" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - otelcodes "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" - "go.opentelemetry.io/otel/propagation" - sdkmetric "go.opentelemetry.io/otel/sdk/metric" - sdkresource "go.opentelemetry.io/otel/sdk/resource" - sdktrace "go.opentelemetry.io/otel/sdk/trace" - "go.opentelemetry.io/otel/trace" - - otelhooks "github.com/open-feature/go-sdk-contrib/hooks/open-telemetry/pkg" - flagd "github.com/open-feature/go-sdk-contrib/providers/flagd/pkg" - "github.com/open-feature/go-sdk/openfeature" - pb "github.com/opentelemetry/opentelemetry-demo/src/product-catalog/genproto/oteldemo" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials/insecure" - healthpb "google.golang.org/grpc/health/grpc_health_v1" - "google.golang.org/grpc/reflection" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/encoding/protojson" -) - -var ( - log *logrus.Logger - catalog []*pb.Product - resource *sdkresource.Resource - initResourcesOnce sync.Once -) - -func init() { - log = logrus.New() - var err error - catalog, err = readProductFiles() - if err != nil { - log.Fatalf("Reading Product Files: %v", err) - os.Exit(1) - } -} - -func initResource() *sdkresource.Resource { - initResourcesOnce.Do(func() { - extraResources, _ := sdkresource.New( - context.Background(), - sdkresource.WithOS(), - sdkresource.WithProcess(), - sdkresource.WithContainer(), - sdkresource.WithHost(), - ) - resource, _ = sdkresource.Merge( - sdkresource.Default(), - extraResources, - ) - }) - return resource -} - -func initTracerProvider() *sdktrace.TracerProvider { - ctx := context.Background() - - exporter, err := otlptracegrpc.New(ctx) - if err != nil { - log.Fatalf("OTLP Trace gRPC Creation: %v", err) - } - tp := sdktrace.NewTracerProvider( - sdktrace.WithBatcher(exporter), - sdktrace.WithResource(initResource()), - ) - otel.SetTracerProvider(tp) - otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) - return tp -} - -func initMeterProvider() *sdkmetric.MeterProvider { - ctx := context.Background() - - exporter, err := otlpmetricgrpc.New(ctx) - if err != nil { - log.Fatalf("new otlp metric grpc exporter failed: %v", err) - } - - mp := sdkmetric.NewMeterProvider( - sdkmetric.WithReader(sdkmetric.NewPeriodicReader(exporter)), - sdkmetric.WithResource(initResource()), - ) - otel.SetMeterProvider(mp) - return mp -} - -func main() { - tp := initTracerProvider() - defer func() { - if err := tp.Shutdown(context.Background()); err != nil { - log.Fatalf("Tracer Provider Shutdown: %v", err) - } - log.Println("Shutdown tracer provider") - }() - - mp := initMeterProvider() - defer func() { - if err := mp.Shutdown(context.Background()); err != nil { - log.Fatalf("Error shutting down meter provider: %v", err) - } - log.Println("Shutdown meter provider") - }() - openfeature.AddHooks(otelhooks.NewTracesHook()) - err := openfeature.SetProvider(flagd.NewProvider()) - if err != nil { - log.Fatal(err) - } - - err = runtime.Start(runtime.WithMinimumReadMemStatsInterval(time.Second)) - if err != nil { - log.Fatal(err) - } - - svc := &productCatalog{} - var port string - mustMapEnv(&port, "PRODUCT_CATALOG_PORT") - - log.Infof("Product Catalog gRPC server started on port: %s", port) - - ln, err := net.Listen("tcp", fmt.Sprintf(":%s", port)) - if err != nil { - log.Fatalf("TCP Listen: %v", err) - } - - srv := grpc.NewServer( - grpc.StatsHandler(otelgrpc.NewServerHandler()), - ) - - reflection.Register(srv) - - pb.RegisterProductCatalogServiceServer(srv, svc) - healthpb.RegisterHealthServer(srv, svc) - - ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGKILL) - defer cancel() - - go func() { - if err := srv.Serve(ln); err != nil { - log.Fatalf("Failed to serve gRPC server, err: %v", err) - } - }() - - <-ctx.Done() - - srv.GracefulStop() - log.Println("Product Catalog gRPC server stopped") -} - -type productCatalog struct { - pb.UnimplementedProductCatalogServiceServer -} - -func readProductFiles() ([]*pb.Product, error) { - - // find all .json files in the products directory - entries, err := os.ReadDir("./products") - if err != nil { - return nil, err - } - - jsonFiles := make([]fs.FileInfo, 0, len(entries)) - for _, entry := range entries { - if strings.HasSuffix(entry.Name(), ".json") { - info, err := entry.Info() - if err != nil { - return nil, err - } - jsonFiles = append(jsonFiles, info) - } - } - - // read the contents of each .json file and unmarshal into a ListProductsResponse - // then append the products to the catalog - var products []*pb.Product - for _, f := range jsonFiles { - jsonData, err := os.ReadFile("./products/" + f.Name()) - if err != nil { - return nil, err - } - - var res pb.ListProductsResponse - if err := protojson.Unmarshal(jsonData, &res); err != nil { - return nil, err - } - - products = append(products, res.Products...) - } - - log.Infof("Loaded %d products", len(products)) - - return products, nil -} - -func mustMapEnv(target *string, key string) { - value, present := os.LookupEnv(key) - if !present { - log.Fatalf("Environment Variable Not Set: %q", key) - } - *target = value -} - -func (p *productCatalog) Check(ctx context.Context, req *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) { - return &healthpb.HealthCheckResponse{Status: healthpb.HealthCheckResponse_SERVING}, nil -} - -func (p *productCatalog) Watch(req *healthpb.HealthCheckRequest, ws healthpb.Health_WatchServer) error { - return status.Errorf(codes.Unimplemented, "health check via Watch not implemented") -} - -func (p *productCatalog) ListProducts(ctx context.Context, req *pb.Empty) (*pb.ListProductsResponse, error) { - span := trace.SpanFromContext(ctx) - - span.SetAttributes( - attribute.Int("app.products.count", len(catalog)), - ) - return &pb.ListProductsResponse{Products: catalog}, nil -} - -func (p *productCatalog) GetProduct(ctx context.Context, req *pb.GetProductRequest) (*pb.Product, error) { - span := trace.SpanFromContext(ctx) - span.SetAttributes( - attribute.String("app.product.id", req.Id), - ) - - // GetProduct will fail on a specific product when feature flag is enabled - if p.checkProductFailure(ctx, req.Id) { - msg := fmt.Sprintf("Error: Product Catalog Fail Feature Flag Enabled") - span.SetStatus(otelcodes.Error, msg) - span.AddEvent(msg) - return nil, status.Errorf(codes.Internal, msg) - } - - var found *pb.Product - for _, product := range catalog { - if req.Id == product.Id { - found = product - break - } - } - - if found == nil { - msg := fmt.Sprintf("Product Not Found: %s", req.Id) - span.SetStatus(otelcodes.Error, msg) - span.AddEvent(msg) - return nil, status.Errorf(codes.NotFound, msg) - } - - msg := fmt.Sprintf("Product Found - ID: %s, Name: %s", req.Id, found.Name) - span.AddEvent(msg) - span.SetAttributes( - attribute.String("app.product.name", found.Name), - ) - return found, nil -} - -func (p *productCatalog) SearchProducts(ctx context.Context, req *pb.SearchProductsRequest) (*pb.SearchProductsResponse, error) { - span := trace.SpanFromContext(ctx) - - var result []*pb.Product - for _, product := range catalog { - if strings.Contains(strings.ToLower(product.Name), strings.ToLower(req.Query)) || - strings.Contains(strings.ToLower(product.Description), strings.ToLower(req.Query)) { - result = append(result, product) - } - } - span.SetAttributes( - attribute.Int("app.products_search.count", len(result)), - ) - return &pb.SearchProductsResponse{Results: result}, nil -} - -func (p *productCatalog) checkProductFailure(ctx context.Context, id string) bool { - if id != "OLJCESPC7Z" { - return false - } - - client := openfeature.NewClient("productCatalog") - failureEnabled, _ := client.BooleanValue( - ctx, "productCatalogFailure", false, openfeature.EvaluationContext{}, - ) - return failureEnabled -} - -func createClient(ctx context.Context, svcAddr string) (*grpc.ClientConn, error) { - return grpc.DialContext(ctx, svcAddr, - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithStatsHandler(otelgrpc.NewClientHandler()), - ) -} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# est-harshname: product-catalog-ci + +on: + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: checkout code + uses: actions/checkout@v4 + + - name: Setup Go 1.22 + uses: actions/setup-go@v5 # Using a more recent version + with: + go-version: '1.22' + + - name: Build + run: | + cd src/product-catalog + go mod download + go build -o product-catalog-service main.go + + - name: unit tests + run: | + cd src/product-catalog + go test ./... + + code-quality: + runs-on: ubuntu-latest + steps: + - name: checkout code + uses: actions/checkout@v4 + + - name: Setup Go 1.22 + uses: actions/setup-go@v5 # Using a more recent version + with: + go-version: '1.22' + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v1.55.2 + # 'run:' is not a valid parameter for this action, the command runs by default + working-directory: src/product-catalog + + docker: + runs-on: ubuntu-latest + needs: build + steps: + - name: checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 # Using a more recent version + + - name: Login to Docker + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Docker Push + uses: docker/build-push-action@v6 + with: + context: src/product-catalog + file: src/product-catalog/Dockerfile + push: true + tags: ${{ secrets.DOCKER_USERNAME }}/product-catalog:${{ github.run_id }} + + updatek8s: + runs-on: ubuntu-latest + needs: docker + steps: + - name: checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Update tag in kubernetes deployment manifest + run: | + sed -i "s|image: .*|image: ${{ secrets.DOCKER_USERNAME }}/product-catalog:${{ github.run_id }}|" kubernetes/productcatalog/deploy.yaml + + + + + - name: Commit and push changes + run: | + git config --global user.email "estharsh@gmail.com" + git config --global user.name "Harsh Pratap" + git add kubernetes/productcatalog/deploy.yaml + git commit -m "[CI]: Update product catalog image tag" + git push origin HEAD:main -f \ No newline at end of file