diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 35c0c4ef..fda02cad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,13 +1,109 @@ -name: Release +name: CI & Release on: push: branches: - main + pull_request: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true jobs: + common-setup: + runs-on: ubuntu-latest + outputs: + should_release: ${{ steps.check_release.outputs.should_release }} + + steps: + - name: Checkout Code + uses: actions/checkout@v5 + + - name: Print Go Version + run: go version + + - name: Get last commit message + id: check_commit + run: | + message=$(git log -1 --pretty=%B) + message="${message//'%'/'%25'}" # Escape '%' + message="${message//$'\n'/'%0A'}" # Escape newlines + message="${message//$'\r'/'%0D'}" # Escape carriage returns + echo "message=$message" >> "$GITHUB_OUTPUT" + shell: bash + + - name: Check Commit Message + run: | + echo "Commit Message: ${{ steps.check_commit.outputs.message }}" + + - name: Check if release should be triggered + id: check_release + run: | + if [[ "${{ steps.check_commit.outputs.message }}" == *"publish"* ]]; then + echo "should_release=true" >> "$GITHUB_OUTPUT" + echo "Debian/RPM packages will be created" + else + echo "should_release=false" >> "$GITHUB_OUTPUT" + echo "Debian/RPM packages will not be created" + fi + + build-and-test: + needs: common-setup + runs-on: ubuntu-latest + permissions: + contents: write # needed to push coverage to gh-pages + pull-requests: write # needed to comment on PRs + + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Install system dependencies (bc + docker) + run: | + sudo apt-get update -qq + sudo apt-get install -y bc docker-ce-cli + + - name: Prepare environment + run: | + export GO111MODULE=on + go mod tidy + make deps + + - name: Check code formatting + run: make fmt + + - name: Run unit tests + run: make test + + - name: Generate coverage + run: | + make coverage + ./scripts/calculateCoverage.sh + touch "Passing" || touch "Failed" + + - name: Build driver image + run: | + make driver + env: + RHSM_USER: ${{ secrets.RHSM_USER }} + RHSM_PASS: ${{ secrets.RHSM_PASS }} + + - name: Publish coverage + if: success() + env: + GHE_TOKEN: ${{ secrets.GHE_TOKEN }} + run: | + ./scripts/publishCoverage.sh + release: - permissions: write-all + needs: common-setup + if: needs.common-setup.outputs.should_release == 'true' + permissions: write-all runs-on: ubuntu-latest strategy: @@ -20,55 +116,37 @@ jobs: APP_VERSION: 1.0.5 steps: - - name: Checkout Code - uses: actions/checkout@v5 - - - name: Print Go Version - run: go version - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: "go" - - - name: Run Unit Tests for cos csi mounter - run: sudo make ut-coverage -C ${{ matrix.package_dir }} + - name: Checkout Code + uses: actions/checkout@v5 - - name: Build Debian and RPM packages for cos-csi-mounter systemd service - run: | + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: "go" + + - name: Build Debian and RPM packages for cos-csi-mounter systemd service + run: | cd ${{ matrix.package_dir }} make packages - - name: Get last commit message - id: check_commit - run: | - message=$(git log -1 --pretty=%B) - message="${message//'%'/'%25'}" # Escape '%' - message="${message//$'\n'/'%0A'}" # Escape newlines - message="${message//$'\r'/'%0D'}" # Escape carriage returns - echo "message=$message" >> "$GITHUB_OUTPUT" - shell: bash - - - name: Check Commit Message - run: | - echo "Commit Message: ${{ steps.check_commit.outputs.message }}" + - name: Latest Version (Tag and Release) + if: success() + id: release + uses: softprops/action-gh-release@v2 + with: + files: | + /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz + /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 + /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz + /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 + tag_name: v1.0.5 + name: v1.0.5 + body: | + ## 🚀 What’s New + - Fix for rclone mount hang issue + - Add support for s3fs disable_noobj_cache flag + - Skip unmount for 'is not a mountpoint' error + prerelease: ${{ env.IS_LATEST_RELEASE != 'true' }} - - name: Latest Version (Tag and Release) - id: release - if: contains(steps.check_commit.outputs.message, 'publish') - uses: softprops/action-gh-release@v2 - with: - files: | - /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz - /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.deb.tar.gz.sha256 - /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz - /home/runner/work/ibm-object-csi-driver/ibm-object-csi-driver/cos-csi-mounter/cos-csi-mounter-${{ env.APP_VERSION }}.rpm.tar.gz.sha256 - tag_name: v1.0.5 - name: v1.0.5 - body: | - ## 🚀 What’s New - - Fix for rclone mount hang issue - prerelease: ${{ env.IS_LATEST_RELEASE != 'true' }} - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 91079242..086affc6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: # You are encouraged to use static refs such as tags, instead of branch name # # Running "pre-commit autoupdate" would automatically updates rev to latest tag - rev: 0.13.1+ibm.62.dss + rev: 0.13.1+ibm.64.dss hooks: - id: detect-secrets # pragma: whitelist secret # Add options for detect-secrets-hook binary. You can run `detect-secrets-hook --help` to list out all possible options. diff --git a/.secrets.baseline b/.secrets.baseline index 4519fb7b..675075a7 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "go.sum|^.secrets.baseline$", "lines": null }, - "generated_at": "2025-09-05T07:59:11Z", + "generated_at": "2025-12-18T07:17:56Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -126,14 +126,14 @@ { "hashed_secret": "a1f0e99af8b76b514ef9e6a174e9c79970332082", "is_verified": false, - "line_number": 174, + "line_number": 176, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "b732fb611fd46a38e8667f9972e0cde777fbe37f", "is_verified": false, - "line_number": 177, + "line_number": 179, "type": "Secret Keyword", "verified_result": null }, @@ -172,7 +172,7 @@ { "hashed_secret": "7e6a3680012346b94b54731e13d8a9ffa3790645", "is_verified": false, - "line_number": 239, + "line_number": 248, "type": "Secret Keyword", "verified_result": null } @@ -227,7 +227,7 @@ { "hashed_secret": "2e7a7ee14caebf378fc32d6cf6f557f347c96773", "is_verified": false, - "line_number": 31, + "line_number": 45, "type": "Secret Keyword", "verified_result": null } @@ -294,7 +294,7 @@ } ] }, - "version": "0.13.1+ibm.62.dss", + "version": "0.13.1+ibm.64.dss", "word_list": { "file": null, "hash": null diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a1efe889..00000000 --- a/.travis.yml +++ /dev/null @@ -1,35 +0,0 @@ ---- -dist: bionic -language: go -go: - - 1.25.0 - -group: bluezone - -matrix: - fast_finish: true - allow_failures: - - go: tip - include: - - os: linux - env: MAKE_TASK="fmt" - - os: linux - env: MAKE_TASK="test-sanity" - - os: linux - env: MAKE_TASK="coverage" - -cache: - bundler: true - -sudo: true -services: - - docker - -before_script: - - sudo apt-get update - - make $MAKE_TASK - -script: - if [[ "$MAKE_TASK" == "fmt" ]]; then - make driver; - fi diff --git a/Dockerfile b/Dockerfile index 0eff9778..6e57b4e0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -64,4 +64,4 @@ RUN yum update -y && yum install fuse fuse-libs fuse3 fuse3-libs -y COPY --from=s3fs-builder /usr/local/bin/s3fs /usr/bin/s3fs COPY --from=rclone-builder /usr/local/bin/rclone /usr/bin/rclone COPY ibm-object-csi-driver ibm-object-csi-driver -ENTRYPOINT ["/ibm-object-csi-driver"] +ENTRYPOINT ["/ibm-object-csi-driver"] \ No newline at end of file diff --git a/Makefile b/Makefile index 9931327e..e1fe648b 100644 --- a/Makefile +++ b/Makefile @@ -3,16 +3,14 @@ EXE_DRIVER_NAME=ibm-object-csi-driver REGISTRY=quay.io/ibm-object-csi-driver -export LINT_VERSION="2.3.1" +export LINT_VERSION="2.7.2" COLOR_YELLOW=\033[0;33m COLOR_RESET=\033[0m GOFILES=$(shell find . -type f -name '*.go' -not -path "./vendor/*") - all: build - .PHONY: build-% clean REV=$(shell git describe --long --tags --match='v*' --dirty 2>/dev/null || git rev-list -n1 HEAD) @@ -33,9 +31,10 @@ test: .PHONY: deps deps: - echo "Installing dependencies ..." + @echo "Installing dependencies ..." @if ! which golangci-lint >/dev/null || [[ "$$(golangci-lint --version)" != *${LINT_VERSION}* ]]; then \ - go install github.com/golangci/golangci-lint/cmd/golangci-lint@v${LINT_VERSION}; \ + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \ + sh -s -- -b $(shell go env GOPATH)/bin v${LINT_VERSION}; \ fi .PHONY: fmt @@ -44,9 +43,10 @@ fmt: lint .PHONY: coverage coverage: test - cat coverage.out | grep -v /fake > cover.out; - # go tool cover -html=cover.out -o=cover.html - go tool cover -func=cover.out | fgrep total + cat coverage.out | grep -v /fake > cover.out + go tool cover -html=cover.out -o cover.html + @echo "Coverage report: cover.html" + @./scripts/calculateCoverage.sh clean: -rm -rf bin diff --git a/cos-csi-mounter/Makefile b/cos-csi-mounter/Makefile index 4ec174d7..55da3103 100644 --- a/cos-csi-mounter/Makefile +++ b/cos-csi-mounter/Makefile @@ -18,14 +18,6 @@ RPM_ARCH := x86_64 RPM_RELEASE_NUM := 1 REDHAT_SPEC := $(BUILD_DIR)/red-hat.spec -test: - go test -v -timeout 1800s -coverprofile=cover.out ./... - go tool cover -html=cover.out -o=cover.html - -ut-coverage: test - @./scripts/coverage.sh - rm cover.html cover.out - build-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod mod -o ${BIN_DIR}/cos-csi-mounter-server -ldflags "-s -w -X main.Version=$(APP_VERSION) -X main.GitCommit=$$(git rev-parse HEAD)" -a ./server ./${BIN_DIR}/cos-csi-mounter-server version diff --git a/pkg/mounter/mounter_test.go b/pkg/mounter/mounter_test.go index d34f9146..fd5543df 100644 --- a/pkg/mounter/mounter_test.go +++ b/pkg/mounter/mounter_test.go @@ -1,14 +1,28 @@ package mounter import ( + "reflect" + "sort" + "testing" + "github.com/IBM/ibm-object-csi-driver/pkg/constants" mounterUtils "github.com/IBM/ibm-object-csi-driver/pkg/mounter/utils" "github.com/stretchr/testify/assert" - - "reflect" - "testing" ) +func stringSlicesEqualIgnoreOrder(a, b []string) bool { + if len(a) != len(b) { + return false + } + aCopy := make([]string, len(a)) + bCopy := make([]string, len(b)) + copy(aCopy, a) + copy(bCopy, b) + sort.Strings(aCopy) + sort.Strings(bCopy) + return reflect.DeepEqual(aCopy, bCopy) +} + func TestNewMounter(t *testing.T) { tests := []struct { name string @@ -41,7 +55,7 @@ func TestNewMounter(t *testing.T) { AuthType: "iam", KpRootKeyCrn: "test-kp-root-key-crn", MountOptions: []string{"opt1=val1", "cipher_suites=default"}, - MounterUtils: &(mounterUtils.MounterOptsUtils{}), + MounterUtils: &mounterUtils.MounterOptsUtils{}, }, expectedErr: nil, }, @@ -71,7 +85,7 @@ func TestNewMounter(t *testing.T) { UID: "fake-uid", GID: "fake-gid", MountOptions: []string{"opt1=val1", "opt2=val2"}, - MounterUtils: &(mounterUtils.MounterOptsUtils{}), + MounterUtils: &mounterUtils.MounterOptsUtils{}, }, expectedErr: nil, }, @@ -97,7 +111,7 @@ func TestNewMounter(t *testing.T) { AuthType: "hmac", KpRootKeyCrn: "test-kp-root-key-crn", MountOptions: []string{"cipher_suites=default"}, - MounterUtils: &(mounterUtils.MounterOptsUtils{}), + MounterUtils: &mounterUtils.MounterOptsUtils{}, }, expectedErr: nil, }, @@ -109,6 +123,23 @@ func TestNewMounter(t *testing.T) { result := factory.NewMounter(test.attrib, test.secretMap, test.mountOptions, nil) + if s3fs, ok := result.(*S3fsMounter); ok { + expected := test.expected.(*S3fsMounter) + if !stringSlicesEqualIgnoreOrder(s3fs.MountOptions, expected.MountOptions) { + t.Errorf("MountOptions mismatch.\nGot: %v\nWant: %v", s3fs.MountOptions, expected.MountOptions) + } + s3fs.MountOptions = nil + expected.MountOptions = nil + } + if rclone, ok := result.(*RcloneMounter); ok { + expected := test.expected.(*RcloneMounter) + if !stringSlicesEqualIgnoreOrder(rclone.MountOptions, expected.MountOptions) { + t.Errorf("MountOptions mismatch.\nGot: %v\nWant: %v", rclone.MountOptions, expected.MountOptions) + } + rclone.MountOptions = nil + expected.MountOptions = nil + } + assert.Equal(t, result, test.expected) if !reflect.DeepEqual(result, test.expected) { diff --git a/scripts/calculateCoverage.sh b/scripts/calculateCoverage.sh new file mode 100755 index 00000000..85f6d4ef --- /dev/null +++ b/scripts/calculateCoverage.sh @@ -0,0 +1,19 @@ +#!/bin/bash +#****************************************************************************** +# * Licensed Materials - Property of IBM +# * IBM Cloud Kubernetes Service, 5737-D43 +# * (C) Copyright IBM Corp. 2025 All Rights Reserved. +# * US Government Users Restricted Rights - Use, duplication or +# * disclosure restricted by GSA ADP Schedule Contract with IBM Corp. +#****************************************************************************** +# +# This script calculates the test coverage from cover.html and outputs the percentage. +# +# It is called by the GitHub Action in the pipeline to calculate the coverage percentage. + +# Extract the coverage percentage from cover.html + +COVERAGE=$(cat cover.html | grep "%)" | sed 's/[][()><%]/ /g' | awk '{ print $4 }' | awk '{s+=$1}END{print s/NR}') +echo "-------------------------------------------------------------------------" +echo "COVERAGE IS ${COVERAGE}%" +echo "-------------------------------------------------------------------------" \ No newline at end of file diff --git a/scripts/publishCoverage.sh b/scripts/publishCoverage.sh new file mode 100755 index 00000000..a1ad8bdc --- /dev/null +++ b/scripts/publishCoverage.sh @@ -0,0 +1,111 @@ +#!/bin/bash +#****************************************************************************** +# * Licensed Materials - Property of IBM +# * IBM Cloud Kubernetes Service, 5737-D43 +# * (C) Copyright IBM Corp. 2025 All Rights Reserved. +# * US Government Users Restricted Rights - Use, duplication or +# * disclosure restricted by GSA ADP Schedule Contract with IBM Corp. +#****************************************************************************** +# +# This script publishes the coverage results to the PR comment and the GitHub Pages +# coverage badge. It calculates and compares the coverage between branches and posts +# a comment to the pull request with the coverage result. +set -euo pipefail + +echo "===== Publishing the coverage results =====" + +WORKDIR="$GITHUB_WORKSPACE/gh-pages" +NEW_COVERAGE_SOURCE="$GITHUB_WORKSPACE/cover.html" +BADGE_COLOR="red" +GREEN_THRESHOLD=85 +YELLOW_THRESHOLD=50 + +# Helper: extract coverage % from cover.html +get_coverage() { + local file="$1" + if [[ -f "$file" ]]; then + grep "%)" "$file" \ + | sed 's/[][()><%]/ /g' \ + | awk '{s+=$4}END{if(NR>0)print s/NR; else print 0}' + else + echo "0" + fi +} + +# Base branch for comparison +if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then + BASE_BRANCH="$GITHUB_BASE_REF" +else + BASE_BRANCH="$GITHUB_REF_NAME" +fi + +# Calculate new coverage +NEW_COVERAGE=$(get_coverage "$NEW_COVERAGE_SOURCE") +NEW_COVERAGE=$(printf "%.2f" "$NEW_COVERAGE") + +# Clone gh-pages +mkdir -p "$WORKDIR" +cd "$WORKDIR" + +if ! git clone -q -b gh-pages "https://x-access-token:$GHE_TOKEN@github.com/$GITHUB_REPOSITORY.git" . 2>/dev/null; then + echo "gh-pages branch not found → creating it" + git init -q + git checkout -b gh-pages +fi + +git config user.name "github-actions[bot]" +git config user.email "github-actions[bot]@users.noreply.github.com" + +# Calculate old coverage +COVERAGE_DIR="coverage/$BASE_BRANCH" +OLD_COVER_HTML="$COVERAGE_DIR/cover.html" +OLD_COVERAGE=$(get_coverage "$OLD_COVER_HTML") +OLD_COVERAGE=$(printf "%.2f" "$OLD_COVERAGE") + +echo "===== Coverage comparison =====" +echo "Old Coverage: $OLD_COVERAGE%" +echo "New Coverage: $NEW_COVERAGE%" + +# Update reports +mkdir -p "$COVERAGE_DIR" +mkdir -p "coverage/$GITHUB_SHA" +cp "$NEW_COVERAGE_SOURCE" "$COVERAGE_DIR/cover.html" +cp "$NEW_COVERAGE_SOURCE" "coverage/$GITHUB_SHA/cover.html" + +# Badge color +if (( $(echo "$NEW_COVERAGE > $GREEN_THRESHOLD" | bc -l) )); then + BADGE_COLOR="green" +elif (( $(echo "$NEW_COVERAGE > $YELLOW_THRESHOLD" | bc -l) )); then + BADGE_COLOR="yellow" +fi + +curl -s "https://img.shields.io/badge/coverage-${NEW_COVERAGE}%25-${BADGE_COLOR}.svg" \ + > "$COVERAGE_DIR/badge.svg" + +# Result message +if (( $(echo "$OLD_COVERAGE > $NEW_COVERAGE" | bc -l) )); then + RESULT_MESSAGE=":red_circle: Coverage decreased from $OLD_COVERAGE% to $NEW_COVERAGE%" +elif (( $(echo "$OLD_COVERAGE == $NEW_COVERAGE" | bc -l) )); then + RESULT_MESSAGE=":thumbsup: Coverage remained same at $NEW_COVERAGE%" +else + RESULT_MESSAGE=":thumbsup: Coverage increased from $OLD_COVERAGE% to $NEW_COVERAGE%" +fi + +# Push to gh-pages (only on push) +if [[ "$GITHUB_EVENT_NAME" == "push" ]]; then + git add . + git commit -m "Coverage: $GITHUB_SHA (run $GITHUB_RUN_NUMBER)" || echo "Nothing to commit" + git push "https://x-access-token:$GHE_TOKEN@github.com/$GITHUB_REPOSITORY.git" gh-pages +fi + +# Comment on PR +if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then + PR_NUMBER=$(jq -r .pull_request.number "$GITHUB_EVENT_PATH") + curl -s -X POST \ + -H "Authorization: token $GHE_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"body\": \"$RESULT_MESSAGE\"}" \ + "https://api.github.com/repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" +fi + +echo "===== Coverage publishing finished =====" \ No newline at end of file