#!/usr/bin/env bash
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you 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.

#
# Copyright 2023 The Kubernetes 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.

# This presents several functions for packages which want to use kubernetes
# code-generation tools.

# These functions insist that your input IDL (commented go) files be located in
# go packages following the pattern $input_pkg_root/$something_sans_slash/$api_version .
# Those $something_sans_slash will be propagated into the output directory structure.

set -o errexit
set -o nounset
set -o pipefail

KUBE_CODEGEN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"

function kube::codegen::internal::findz() {
    # We use `find` rather than `git ls-files` because sometimes external
    # projects use this across repos.  This is an imperfect wrapper of find,
    # but good enough for this script.
    find "$@" -print0
}

function kube::codegen::internal::grep() {
    # We use `grep` rather than `git grep` because sometimes external projects
    # use this across repos.
    grep "$@" \
        --exclude-dir .git \
        --exclude-dir _output \
        --exclude-dir vendor
}

# Generate tagged helper code: conversions, deepcopy, and defaults
#
# USAGE: kube::codegen::gen_helpers [FLAGS] <input-dir>
#
# <input-dir>
#   The root directory under which to search for Go files which request code to
#   be generated.  This must be a local path, not a Go package.
#
#   See note at the top about package structure below that.
#
# FLAGS:
#
#   --boilerplate <string = path_to_kube_codegen_boilerplate>
#     An optional override for the header file to insert into generated files.
#
#   --extra-peer-dir <string>
#     An optional list (this flag may be specified multiple times) of "extra"
#     directories to consider during conversion generation.
#
function kube::codegen::gen_helpers() {
    local in_dir=""
    local boilerplate="${KUBE_CODEGEN_ROOT}/hack/boilerplate.go.txt"
    local v="${KUBE_VERBOSE:-0}"
    local extra_peers=()

    while [ "$#" -gt 0 ]; do
        case "$1" in
            "--boilerplate")
                boilerplate="$2"
                shift 2
                ;;
            "--extra-peer-dir")
                extra_peers+=("$2")
                shift 2
                ;;
            *)
                if [[ "$1" =~ ^-- ]]; then
                    echo "unknown argument: $1" >&2
                    return 1
                fi
                if [ -n "$in_dir" ]; then
                    echo "too many arguments: $1 (already have $in_dir)" >&2
                    return 1
                fi
                in_dir="$1"
                shift
                ;;
        esac
    done

    if [ -z "${in_dir}" ]; then
        echo "input-dir argument is required" >&2
        return 1
    fi

    (
        # To support running this from anywhere, first cd into this directory,
        # and then install with forced module mode on and fully qualified name.
        cd "${KUBE_CODEGEN_ROOT}"
        BINS=(
            conversion-gen
            deepcopy-gen
            defaulter-gen
        )
        # shellcheck disable=2046 # printf word-splitting is intentional
        GO111MODULE=on go install $(printf "k8s.io/code-generator/cmd/%s@v0.30.5 " "${BINS[@]}")
    )
    # Go installs in $GOBIN if defined, and $GOPATH/bin otherwise
    gobin="${GOBIN:-$(go env GOPATH)/bin}"

    # Deepcopy
    #
    local input_pkgs=()
    while read -r dir; do
        pkg="$(cd "${dir}" && GO111MODULE=on go list -find .)"
        input_pkgs+=("${pkg}")
    done < <(
        ( kube::codegen::internal::grep -l --null \
            -e '^\s*//\s*+k8s:deepcopy-gen=' \
            -r "${in_dir}" \
            --include '*.go' \
            || true \
        ) | while read -r -d $'\0' F; do dirname "${F}"; done \
          | LC_ALL=C sort -u
    )

    if [ "${#input_pkgs[@]}" != 0 ]; then
        echo "Generating deepcopy code for ${#input_pkgs[@]} targets"

        kube::codegen::internal::findz \
            "${in_dir}" \
            -type f \
            -name zz_generated.deepcopy.go \
            | xargs -0 rm -f

        "${gobin}/deepcopy-gen" \
            -v "${v}" \
            --output-file zz_generated.deepcopy.go \
            --go-header-file "${boilerplate}" \
            "${input_pkgs[@]}"
    fi

    # Defaults
    #
    local input_pkgs=()
    while read -r dir; do
        pkg="$(cd "${dir}" && GO111MODULE=on go list -find .)"
        input_pkgs+=("${pkg}")
    done < <(
        ( kube::codegen::internal::grep -l --null \
            -e '^\s*//\s*+k8s:defaulter-gen=' \
            -r "${in_dir}" \
            --include '*.go' \
            || true \
        ) | while read -r -d $'\0' F; do dirname "${F}"; done \
          | LC_ALL=C sort -u
    )

    if [ "${#input_pkgs[@]}" != 0 ]; then
        echo "Generating defaulter code for ${#input_pkgs[@]} targets"

        kube::codegen::internal::findz \
            "${in_dir}" \
            -type f \
            -name zz_generated.defaults.go \
            | xargs -0 rm -f

        "${gobin}/defaulter-gen" \
            -v "${v}" \
            --output-file zz_generated.defaults.go \
            --go-header-file "${boilerplate}" \
            "${input_pkgs[@]}"
    fi

    # Conversions
    #
    local input_pkgs=()
    while read -r dir; do
        pkg="$(cd "${dir}" && GO111MODULE=on go list -find .)"
        input_pkgs+=("${pkg}")
    done < <(
        ( kube::codegen::internal::grep -l --null \
            -e '^\s*//\s*+k8s:conversion-gen=' \
            -r "${in_dir}" \
            --include '*.go' \
            || true \
        ) | while read -r -d $'\0' F; do dirname "${F}"; done \
          | LC_ALL=C sort -u
    )

    if [ "${#input_pkgs[@]}" != 0 ]; then
        echo "Generating conversion code for ${#input_pkgs[@]} targets"

        kube::codegen::internal::findz \
            "${in_dir}" \
            -type f \
            -name zz_generated.conversion.go \
            | xargs -0 rm -f

        local extra_peer_args=()
        for arg in "${extra_peers[@]:+"${extra_peers[@]}"}"; do
            extra_peer_args+=("--extra-peer-dirs" "$arg")
        done
        "${gobin}/conversion-gen" \
            -v "${v}" \
            --output-file zz_generated.conversion.go \
            --go-header-file "${boilerplate}" \
            "${extra_peer_args[@]:+"${extra_peer_args[@]}"}" \
            "${input_pkgs[@]}"
    fi
}

# Generate openapi code
#
# USAGE: kube::codegen::gen_openapi [FLAGS] <input-dir>
#
# <input-dir>
#   The root directory under which to search for Go files which request openapi
#   to be generated.  This must be a local path, not a Go package.
#
#   See note at the top about package structure below that.
#
# FLAGS:
#
#   --output-dir <string>
#     The directory into which to emit code.
#
#   --output-pkg <string>
#     The Go package path (import path) of the --output-dir.
#
#   --extra-pkgs <string>
#     An optional list of additional packages to be imported during openapi
#     generation.  The argument must be Go package syntax, e.g.
#     "k8s.io/foo/bar".  It may be a single value or a comma-delimited list.
#     This flag may be repeated.
#
#   --report-filename <string = "/dev/null">
#     An optional path at which to write an API violations report.  "-" means
#     stdout.
#
#   --update-report
#     If specified, update the report file in place, rather than diffing it.
#
#   --boilerplate <string = path_to_kube_codegen_boilerplate>
#     An optional override for the header file to insert into generated files.
#
function kube::codegen::gen_openapi() {
    local in_dir=""
    local out_dir=""
    local out_pkg=""
    local extra_pkgs=()
    local report="/dev/null"
    local update_report=""
    local boilerplate="${KUBE_CODEGEN_ROOT}/hack/boilerplate.go.txt"
    local v="${KUBE_VERBOSE:-0}"

    while [ "$#" -gt 0 ]; do
        case "$1" in
            "--output-dir")
                out_dir="$2"
                shift 2
                ;;
            "--output-pkg")
                out_pkg="$2"
                shift 2
                ;;
            "--extra-pkgs")
                extra_pkgs+=("$2")
                shift 2
                ;;
            "--report-filename")
                report="$2"
                shift 2
                ;;
            "--update-report")
                update_report="true"
                shift
                ;;
            "--boilerplate")
                boilerplate="$2"
                shift 2
                ;;
            *)
                if [[ "$1" =~ ^-- ]]; then
                    echo "unknown argument: $1" >&2
                    return 1
                fi
                if [ -n "$in_dir" ]; then
                    echo "too many arguments: $1 (already have $in_dir)" >&2
                    return 1
                fi
                in_dir="$1"
                shift
                ;;
        esac
    done

    if [ -z "${in_dir}" ]; then
        echo "input-dir argument is required" >&2
        return 1
    fi
    if [ -z "${out_dir}" ]; then
        echo "--output-dir is required" >&2
        return 1
    fi
    if [ -z "${out_pkg}" ]; then
        echo "--output-pkg is required" >&2
        return 1
    fi

    local new_report
    new_report="$(mktemp -t "$(basename "$0").api_violations.XXXXXX")"
    if [ -n "${update_report}" ]; then
        new_report="${report}"
    fi

    (
        # To support running this from anywhere, first cd into this directory,
        # and then install with forced module mode on and fully qualified name.
        cd "${KUBE_CODEGEN_ROOT}"
        BINS=(
            openapi-gen
        )
        # shellcheck disable=2046 # printf word-splitting is intentional
        GO111MODULE=on go install $(printf "k8s.io/kube-openapi/cmd/%s@v0.30.5 " "${BINS[@]}")
    )
    # Go installs in $GOBIN if defined, and $GOPATH/bin otherwise
    gobin="${GOBIN:-$(go env GOPATH)/bin}"

    local input_pkgs=( "${extra_pkgs[@]:+"${extra_pkgs[@]}"}")
    while read -r dir; do
        pkg="$(cd "${dir}" && GO111MODULE=on go list -find .)"
        input_pkgs+=("${pkg}")
    done < <(
        ( kube::codegen::internal::grep -l --null \
            -e '^\s*//\s*+k8s:openapi-gen=' \
            -r "${in_dir}" \
            --include '*.go' \
            || true \
        ) | while read -r -d $'\0' F; do dirname "${F}"; done \
          | LC_ALL=C sort -u
    )

    if [ "${#input_pkgs[@]}" != 0 ]; then
        echo "Generating openapi code for ${#input_pkgs[@]} targets"

        kube::codegen::internal::findz \
            "${in_dir}" \
            -type f \
            -name zz_generated.openapi.go \
            | xargs -0 rm -f

        "${gobin}/openapi-gen" \
            -v "${v}" \
            --output-file zz_generated.openapi.go \
            --go-header-file "${boilerplate}" \
            --output-dir "${out_dir}" \
            --output-pkg "${out_pkg}" \
            --report-filename "${new_report}" \
            "k8s.io/apimachinery/pkg/apis/meta/v1" \
            "k8s.io/apimachinery/pkg/runtime" \
            "k8s.io/apimachinery/pkg/version" \
            "${input_pkgs[@]}"
    fi

    touch "${report}" # in case it doesn't exist yet
    if ! diff -u "${report}" "${new_report}"; then
        echo -e "ERROR:"
        echo -e "\tAPI rule check failed for ${report}: new reported violations"
        echo -e "\tPlease read api/api-rules/README.md"
        return 1
    fi
}

# Generate client code
#
# USAGE: kube::codegen::gen_client [FLAGS] <input-dir>
#
# <input-dir>
#   The root package under which to search for Go files which request clients
#   to be generated. This must be a local path, not a Go package.
#
#   See note at the top about package structure below that.
#
# FLAGS:
#   --one-input-api <string>
#     A specific API (a directory) under the input-dir for which to generate a
#     client.  If this is not set, clients for all APIs under the input-dir
#     will be generated (under the --output-pkg).
#
#   --output-dir <string>
#     The root directory under which to emit code.  Each aspect of client
#     generation will make one or more subdirectories.
#
#   --output-pkg <string>
#     The Go package path (import path) of the --output-dir.  Each aspect of
#     client generation will make one or more sub-packages.
#
#   --boilerplate <string = path_to_kube_codegen_boilerplate>
#     An optional override for the header file to insert into generated files.
#
#   --clientset-name <string = "clientset">
#     An optional override for the leaf name of the generated "clientset" directory.
#
#   --versioned-name <string = "versioned">
#     An optional override for the leaf name of the generated
#     "<clientset>/versioned" directory.
#
#   --with-applyconfig
#     Enables generation of applyconfiguration files.
#
#   --applyconfig-name <string = "applyconfiguration">
#     An optional override for the leaf name of the generated "applyconfiguration" directory.
#
#   --applyconfig-externals <string = "">
#     An optional list of comma separated external apply configurations locations
#     in <type-package>.<type-name>:<applyconfiguration-package> form.
#
#   --with-watch
#     Enables generation of listers and informers for APIs which support WATCH.
#
#   --listers-name <string = "listers">
#     An optional override for the leaf name of the generated "listers" directory.
#
#   --informers-name <string = "informers">
#     An optional override for the leaf name of the generated "informers" directory.
#
#   --plural-exceptions <string = "">
#     An optional list of comma separated plural exception definitions in Type:PluralizedType form.
#
function kube::codegen::gen_client() {
    local in_dir=""
    local one_input_api=""
    local out_dir=""
    local out_pkg=""
    local clientset_subdir="clientset"
    local clientset_versioned_name="versioned"
    local applyconfig="false"
    local applyconfig_subdir="applyconfiguration"
    local applyconfig_external=""
    local applyconfig_openapi_schema=""
    local watchable="false"
    local listers_subdir="listers"
    local informers_subdir="informers"
    local boilerplate="${KUBE_CODEGEN_ROOT}/hack/boilerplate.go.txt"
    local plural_exceptions=""
    local v="${KUBE_VERBOSE:-0}"

    while [ "$#" -gt 0 ]; do
        case "$1" in
            "--one-input-api")
                one_input_api="/$2"
                shift 2
                ;;
            "--output-dir")
                out_dir="$2"
                shift 2
                ;;
            "--output-pkg")
                out_pkg="$2"
                shift 2
                ;;
            "--boilerplate")
                boilerplate="$2"
                shift 2
                ;;
            "--clientset-name")
                clientset_subdir="$2"
                shift 2
                ;;
            "--versioned-name")
                clientset_versioned_name="$2"
                shift 2
                ;;
            "--with-applyconfig")
                applyconfig="true"
                shift
                ;;
            "--applyconfig-name")
                applyconfig_subdir="$2"
                shift 2
                ;;
            "--applyconfig-externals")
                applyconfig_external="$2"
                shift 2
                ;;
            "--applyconfig-openapi-schema")
                applyconfig_openapi_schema="$2"
                shift 2
                ;;
            "--with-watch")
                watchable="true"
                shift
                ;;
            "--listers-name")
                listers_subdir="$2"
                shift 2
                ;;
            "--informers-name")
                informers_subdir="$2"
                shift 2
                ;;
            "--plural-exceptions")
                plural_exceptions="$2"
                shift 2
                ;;
            *)
                if [[ "$1" =~ ^-- ]]; then
                    echo "unknown argument: $1" >&2
                    return 1
                fi
                if [ -n "$in_dir" ]; then
                    echo "too many arguments: $1 (already have $in_dir)" >&2
                    return 1
                fi
                in_dir="$1"
                shift
                ;;
        esac
    done

    if [ -z "${in_dir}" ]; then
        echo "input-dir argument is required" >&2
        return 1
    fi
    if [ -z "${out_dir}" ]; then
        echo "--output-dir is required" >&2
        return 1
    fi
    if [ -z "${out_pkg}" ]; then
        echo "--output-pkg is required" >&2
    fi

    mkdir -p "${out_dir}"

    (
        # To support running this from anywhere, first cd into this directory,
        # and then install with forced module mode on and fully qualified name.
        cd "${KUBE_CODEGEN_ROOT}"
        BINS=(
            applyconfiguration-gen
            client-gen
            informer-gen
            lister-gen
        )
        # shellcheck disable=2046 # printf word-splitting is intentional
        GO111MODULE=on go install $(printf "k8s.io/code-generator/cmd/%s@v0.30.5 " "${BINS[@]}")
    )
    # Go installs in $GOBIN if defined, and $GOPATH/bin otherwise
    gobin="${GOBIN:-$(go env GOPATH)/bin}"

    local group_versions=()
    local input_pkgs=()
    while read -r dir; do
        pkg="$(cd "${dir}" && GO111MODULE=on go list -find .)"
        leaf="$(basename "${dir}")"
        if grep -E -q '^v[0-9]+((alpha|beta)[0-9]+)?$' <<< "${leaf}"; then
            input_pkgs+=("${pkg}")

            dir2="$(dirname "${dir}")"
            leaf2="$(basename "${dir2}")"
            group_versions+=("${leaf2}/${leaf}")
        fi
    done < <(
        ( kube::codegen::internal::grep -l --null \
            -e '^\s*//\s*+genclient' \
            -r "${in_dir}${one_input_api}" \
            --include '*.go' \
            || true \
        ) | while read -r -d $'\0' F; do dirname "${F}"; done \
          | LC_ALL=C sort -u
    )

    if [ "${#group_versions[@]}" == 0 ]; then
        return 0
    fi

    applyconfig_pkg="" # set this for later use, iff enabled
    if [ "${applyconfig}" == "true" ]; then
        applyconfig_pkg="${out_pkg}/${applyconfig_subdir}"

        echo "Generating applyconfig code for ${#input_pkgs[@]} targets"

        ( kube::codegen::internal::grep -l --null \
            -e '^// Code generated by applyconfiguration-gen. DO NOT EDIT.$' \
            -r "${out_dir}/${applyconfig_subdir}" \
            --include '*.go' \
            || true \
        ) | xargs -0 rm -f

        "${gobin}/applyconfiguration-gen" \
            -v "${v}" \
            --go-header-file "${boilerplate}" \
            --output-dir "${out_dir}/${applyconfig_subdir}" \
            --output-pkg "${applyconfig_pkg}" \
            --external-applyconfigurations "${applyconfig_external}" \
            --openapi-schema "${applyconfig_openapi_schema}" \
            "${input_pkgs[@]}"
    fi

    echo "Generating client code for ${#group_versions[@]} targets"

    ( kube::codegen::internal::grep -l --null \
        -e '^// Code generated by client-gen. DO NOT EDIT.$' \
        -r "${out_dir}/${clientset_subdir}" \
        --include '*.go' \
        || true \
    ) | xargs -0 rm -f

    local inputs=()
    for arg in "${group_versions[@]}"; do
        inputs+=("--input" "$arg")
    done
     "${gobin}/client-gen" \
        -v "${v}" \
        --go-header-file "${boilerplate}" \
        --output-dir "${out_dir}/${clientset_subdir}" \
        --output-pkg "${out_pkg}/${clientset_subdir}" \
        --clientset-name "${clientset_versioned_name}" \
        --apply-configuration-package "${applyconfig_pkg}" \
        --input-base "$(cd "${in_dir}" && pwd -P)" `# must be absolute path or Go import path"` \
        --plural-exceptions "${plural_exceptions}" \
        "${inputs[@]}"

    if [ "${watchable}" == "true" ]; then
        echo "Generating lister code for ${#input_pkgs[@]} targets"

        ( kube::codegen::internal::grep -l --null \
            -e '^// Code generated by lister-gen. DO NOT EDIT.$' \
            -r "${out_dir}/${listers_subdir}" \
            --include '*.go' \
            || true \
        ) | xargs -0 rm -f

        "${gobin}/lister-gen" \
            -v "${v}" \
            --go-header-file "${boilerplate}" \
            --output-dir "${out_dir}/${listers_subdir}" \
            --output-pkg "${out_pkg}/${listers_subdir}" \
            --plural-exceptions "${plural_exceptions}" \
            "${input_pkgs[@]}"

        echo "Generating informer code for ${#input_pkgs[@]} targets"

        ( kube::codegen::internal::grep -l --null \
            -e '^// Code generated by informer-gen. DO NOT EDIT.$' \
            -r "${out_dir}/${informers_subdir}" \
            --include '*.go' \
            || true \
        ) | xargs -0 rm -f

        "${gobin}/informer-gen" \
            -v "${v}" \
            --go-header-file "${boilerplate}" \
            --output-dir "${out_dir}/${informers_subdir}" \
            --output-pkg "${out_pkg}/${informers_subdir}" \
            --versioned-clientset-package "${out_pkg}/${clientset_subdir}/${clientset_versioned_name}" \
            --listers-package "${out_pkg}/${listers_subdir}" \
            --plural-exceptions "${plural_exceptions}" \
            "${input_pkgs[@]}"
    fi
}

# Generate register code
#
# USAGE: kube::codegen::gen_register [FLAGS] <input-dir>
#
# <input-dir>
#   The root directory under which to search for Go files which request code to
#   be generated.  This must be a local path, not a Go package.
#
#   See note at the top about package structure below that.
#
# FLAGS:
#
#   --boilerplate <string = path_to_kube_codegen_boilerplate>
#     An optional override for the header file to insert into generated files.
#
function kube::codegen::gen_register() {
    local in_dir=""
    local boilerplate="${KUBE_CODEGEN_ROOT}/hack/boilerplate.go.txt"
    local v="${KUBE_VERBOSE:-0}"

    while [ "$#" -gt 0 ]; do
        case "$1" in
            "--boilerplate")
                boilerplate="$2"
                shift 2
                ;;
            *)
                if [[ "$1" =~ ^-- ]]; then
                    echo "unknown argument: $1" >&2
                    return 1
                fi
                if [ -n "$in_dir" ]; then
                    echo "too many arguments: $1 (already have $in_dir)" >&2
                    return 1
                fi
                in_dir="$1"
                shift
                ;;
        esac
    done

    if [ -z "${in_dir}" ]; then
        echo "input-dir argument is required" >&2
        return 1
    fi

    (
        # To support running this from anywhere, first cd into this directory,
        # and then install with forced module mode on and fully qualified name.
        cd "${KUBE_CODEGEN_ROOT}"
        BINS=(
            register-gen
        )
        # shellcheck disable=2046 # printf word-splitting is intentional
        GO111MODULE=on go install $(printf "k8s.io/code-generator/cmd/%s@v0.30.5 " "${BINS[@]}")
    )
    # Go installs in $GOBIN if defined, and $GOPATH/bin otherwise
    gobin="${GOBIN:-$(go env GOPATH)/bin}"

    # Register
    #
    local input_pkgs=()
    while read -r dir; do
        pkg="$(cd "${dir}" && GO111MODULE=on go list -find .)"
        input_pkgs+=("${pkg}")
    done < <(
        ( kube::codegen::internal::grep -l --null \
            -e '^\s*//\s*+groupName' \
            -r "${in_dir}" \
            --include '*.go' \
            || true \
        ) | while read -r -d $'\0' F; do dirname "${F}"; done \
          | LC_ALL=C sort -u
    )

    if [ "${#input_pkgs[@]}" != 0 ]; then
        echo "Generating register code for ${#input_pkgs[@]} targets"

        kube::codegen::internal::findz \
            "${in_dir}" \
            -type f \
            -name zz_generated.register.go \
            | xargs -0 rm -f

        "${gobin}/register-gen" \
            -v "${v}" \
            --output-file zz_generated.register.go \
            --go-header-file "${boilerplate}" \
            "${input_pkgs[@]}"
    fi
}
